mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 18:16:38 +08:00
Merge pull request #2872 from woocommerce/PCP-3939-cannot-be-zero-or-negative-with-express-checkout-for-free-trial-subscriptions-on-block-checkout
"CANNOT_BE_ZERO_OR_NEGATIVE" with Express Checkout for Free Trial Subscriptions on Block checkout (3939)
This commit is contained in:
commit
9af375b182
8 changed files with 952 additions and 774 deletions
|
@ -0,0 +1,52 @@
|
|||
import { useMemo } from '@wordpress/element';
|
||||
import { normalizeStyleForFundingSource } from '../../../../ppcp-button/resources/js/modules/Helper/Style';
|
||||
import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js';
|
||||
|
||||
export const BlockEditorPayPalComponent = ( {
|
||||
config,
|
||||
fundingSource,
|
||||
buttonAttributes,
|
||||
} ) => {
|
||||
const urlParams = useMemo(
|
||||
() => ( {
|
||||
clientId: 'test',
|
||||
...config.scriptData.url_params,
|
||||
dataNamespace: 'ppcp-blocks-editor-paypal-buttons',
|
||||
components: 'buttons',
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
const style = useMemo( () => {
|
||||
const configStyle = normalizeStyleForFundingSource(
|
||||
config.scriptData.button.style,
|
||||
fundingSource
|
||||
);
|
||||
|
||||
if ( buttonAttributes ) {
|
||||
return {
|
||||
...configStyle,
|
||||
height: buttonAttributes.height
|
||||
? Number( buttonAttributes.height )
|
||||
: configStyle.height,
|
||||
borderRadius: buttonAttributes.borderRadius
|
||||
? Number( buttonAttributes.borderRadius )
|
||||
: configStyle.borderRadius,
|
||||
};
|
||||
}
|
||||
|
||||
return configStyle;
|
||||
}, [ fundingSource, buttonAttributes ] );
|
||||
|
||||
return (
|
||||
<PayPalScriptProvider options={ urlParams }>
|
||||
<PayPalButtons
|
||||
className={ `ppc-button-container-${ fundingSource }` }
|
||||
fundingSource={ fundingSource }
|
||||
style={ style }
|
||||
forceReRender={ [ buttonAttributes || {} ] }
|
||||
onClick={ () => false }
|
||||
/>
|
||||
</PayPalScriptProvider>
|
||||
);
|
||||
};
|
|
@ -3,10 +3,10 @@ import { useEffect, useState } from '@wordpress/element';
|
|||
import {
|
||||
PayPalScriptProvider,
|
||||
PayPalCardFieldsProvider,
|
||||
PayPalNameField,
|
||||
PayPalNumberField,
|
||||
PayPalExpiryField,
|
||||
PayPalCVVField,
|
||||
PayPalNameField,
|
||||
PayPalNumberField,
|
||||
PayPalExpiryField,
|
||||
PayPalCVVField,
|
||||
} from '@paypal/react-paypal-js';
|
||||
|
||||
import { CheckoutHandler } from './checkout-handler';
|
||||
|
@ -19,11 +19,7 @@ import {
|
|||
import { cartHasSubscriptionProducts } from '../Helper/Subscription';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export function CardFields( {
|
||||
config,
|
||||
eventRegistration,
|
||||
emitResponse,
|
||||
} ) {
|
||||
export function CardFields( { config, eventRegistration, emitResponse } ) {
|
||||
const { onPaymentSetup } = eventRegistration;
|
||||
const { responseTypes } = emitResponse;
|
||||
|
||||
|
@ -96,16 +92,36 @@ export function CardFields( {
|
|||
console.error( err );
|
||||
} }
|
||||
>
|
||||
<PayPalNameField placeholder={ __( 'Cardholder Name (optional)', 'woocommerce-paypal-payments' ) }/>
|
||||
<PayPalNumberField placeholder={ __( 'Card number', 'woocommerce-paypal-payments' ) }/>
|
||||
<div style={ { display: "flex", width: "100%" } }>
|
||||
<div style={ { width: "100%" } }>
|
||||
<PayPalExpiryField placeholder={ __( 'MM / YY', 'woocommerce-paypal-payments' ) }/>
|
||||
</div>
|
||||
<div style={ { width: "100%" } }>
|
||||
<PayPalCVVField placeholder={ __( 'CVV', 'woocommerce-paypal-payments' ) }/>
|
||||
</div>
|
||||
</div>
|
||||
<PayPalNameField
|
||||
placeholder={ __(
|
||||
'Cardholder Name (optional)',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<PayPalNumberField
|
||||
placeholder={ __(
|
||||
'Card number',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<div style={ { display: 'flex', width: '100%' } }>
|
||||
<div style={ { width: '100%' } }>
|
||||
<PayPalExpiryField
|
||||
placeholder={ __(
|
||||
'MM / YY',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
<div style={ { width: '100%' } }>
|
||||
<PayPalCVVField
|
||||
placeholder={ __(
|
||||
'CVV',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CheckoutHandler
|
||||
getCardFieldsForm={ getCardFieldsForm }
|
||||
getSavePayment={ getSavePayment }
|
||||
|
|
14
modules/ppcp-blocks/resources/js/Components/paypal-label.js
Normal file
14
modules/ppcp-blocks/resources/js/Components/paypal-label.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const PaypalLabel = ( { components, config } ) => {
|
||||
const { PaymentMethodIcons } = components;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: config.title,
|
||||
} }
|
||||
/>
|
||||
<PaymentMethodIcons icons={ config.icon } align="right" />
|
||||
</>
|
||||
);
|
||||
};
|
493
modules/ppcp-blocks/resources/js/Components/paypal.js
Normal file
493
modules/ppcp-blocks/resources/js/Components/paypal.js
Normal file
|
@ -0,0 +1,493 @@
|
|||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { loadPayPalScript } from '../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
|
||||
import {
|
||||
mergeWcAddress,
|
||||
paypalAddressToWc,
|
||||
paypalOrderToWcAddresses,
|
||||
} from '../Helper/Address';
|
||||
import { convertKeysToSnakeCase } from '../Helper/Helper';
|
||||
import buttonModuleWatcher from '../../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
|
||||
import { normalizeStyleForFundingSource } from '../../../../ppcp-button/resources/js/modules/Helper/Style';
|
||||
import {
|
||||
cartHasSubscriptionProducts,
|
||||
isPayPalSubscription,
|
||||
} from '../Helper/Subscription';
|
||||
import {
|
||||
createOrder,
|
||||
createSubscription,
|
||||
createVaultSetupToken,
|
||||
handleApprove,
|
||||
handleApproveSubscription,
|
||||
onApproveSavePayment,
|
||||
} from '../paypal-config';
|
||||
|
||||
const PAYPAL_GATEWAY_ID = 'ppcp-gateway';
|
||||
|
||||
const namespace = 'ppcpBlocksPaypalExpressButtons';
|
||||
let registeredContext = false;
|
||||
let paypalScriptPromise = null;
|
||||
|
||||
export const PayPalComponent = ( {
|
||||
config,
|
||||
onClick,
|
||||
onClose,
|
||||
onSubmit,
|
||||
onError,
|
||||
eventRegistration,
|
||||
emitResponse,
|
||||
activePaymentMethod,
|
||||
shippingData,
|
||||
isEditing,
|
||||
fundingSource,
|
||||
buttonAttributes,
|
||||
} ) => {
|
||||
const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } =
|
||||
eventRegistration;
|
||||
const { responseTypes } = emitResponse;
|
||||
|
||||
const [ paypalOrder, setPaypalOrder ] = useState( null );
|
||||
const [ continuationFilled, setContinuationFilled ] = useState( false );
|
||||
const [ gotoContinuationOnError, setGotoContinuationOnError ] =
|
||||
useState( false );
|
||||
|
||||
const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false );
|
||||
|
||||
if ( ! paypalScriptLoaded ) {
|
||||
if ( ! paypalScriptPromise ) {
|
||||
// for editor, since canMakePayment was not called
|
||||
paypalScriptPromise = loadPayPalScript(
|
||||
namespace,
|
||||
config.scriptData
|
||||
);
|
||||
}
|
||||
paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) );
|
||||
}
|
||||
|
||||
const methodId = fundingSource
|
||||
? `${ config.id }-${ fundingSource }`
|
||||
: config.id;
|
||||
|
||||
/**
|
||||
* The block cart displays express checkout buttons. Those buttons are handled by the
|
||||
* PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons").
|
||||
*
|
||||
* A possible bug in WooCommerce does not use the correct payment method ID for the express
|
||||
* payment buttons inside the cart, but sends the ID of the _first_ active payment method.
|
||||
*
|
||||
* This function uses an internal WooCommerce dispatcher method to set the correct method ID.
|
||||
*/
|
||||
const enforcePaymentMethodForCart = () => {
|
||||
// Do nothing, unless we're handling block cart express payment buttons.
|
||||
if ( 'cart-block' !== config.scriptData.context ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the active payment method to PAYPAL_GATEWAY_ID.
|
||||
wp.data
|
||||
.dispatch( 'wc/store/payment' )
|
||||
.__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} );
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
// fill the form if in continuation (for product or mini-cart buttons)
|
||||
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const paypalAddresses = paypalOrderToWcAddresses(
|
||||
config.scriptData.continuation.order
|
||||
);
|
||||
const wcAddresses = wp.data
|
||||
.select( 'wc/store/cart' )
|
||||
.getCustomerData();
|
||||
const addresses = mergeWcAddress( wcAddresses, paypalAddresses );
|
||||
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setBillingAddress( addresses.billingAddress );
|
||||
|
||||
if ( shippingData.needsShipping ) {
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setShippingAddress( addresses.shippingAddress );
|
||||
}
|
||||
} catch ( err ) {
|
||||
// sometimes the PayPal address is missing, skip in this case.
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
// this useEffect should run only once, but adding this in case of some kind of full re-rendering
|
||||
setContinuationFilled( true );
|
||||
}, [ shippingData, continuationFilled ] );
|
||||
|
||||
const getCheckoutRedirectUrl = () => {
|
||||
const checkoutUrl = new URL( config.scriptData.redirect );
|
||||
// sometimes some browsers may load some kind of cached version of the page,
|
||||
// so adding a parameter to avoid that
|
||||
checkoutUrl.searchParams.append(
|
||||
'ppcp-continuation-redirect',
|
||||
new Date().getTime().toString()
|
||||
);
|
||||
return checkoutUrl.toString();
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
const unsubscribe = onCheckoutValidation( () => {
|
||||
if ( config.scriptData.continuation ) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
gotoContinuationOnError &&
|
||||
wp.data.select( 'wc/store/validation' ).hasValidationErrors()
|
||||
) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
return { type: responseTypes.ERROR };
|
||||
}
|
||||
|
||||
return true;
|
||||
} );
|
||||
return unsubscribe;
|
||||
}, [ onCheckoutValidation, gotoContinuationOnError ] );
|
||||
|
||||
const handleClick = ( data, actions ) => {
|
||||
if ( isEditing ) {
|
||||
return actions.reject();
|
||||
}
|
||||
|
||||
window.ppcpFundingSource = data.fundingSource;
|
||||
|
||||
onClick();
|
||||
};
|
||||
|
||||
const shouldHandleShippingInPayPal = () => {
|
||||
return shouldskipFinalConfirmation() && config.needShipping;
|
||||
};
|
||||
|
||||
const shouldskipFinalConfirmation = () => {
|
||||
if ( config.finalReviewEnabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
window.ppcpFundingSource !== 'venmo' ||
|
||||
! config.scriptData.vaultingEnabled
|
||||
);
|
||||
};
|
||||
|
||||
let handleShippingOptionsChange = null;
|
||||
let handleShippingAddressChange = null;
|
||||
|
||||
if ( shippingData.needsShipping && shouldHandleShippingInPayPal() ) {
|
||||
handleShippingOptionsChange = async ( data, actions ) => {
|
||||
try {
|
||||
const shippingOptionId = data.selectedShippingOption?.id;
|
||||
if ( shippingOptionId ) {
|
||||
await wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.selectShippingRate( shippingOptionId );
|
||||
await shippingData.setSelectedRates( shippingOptionId );
|
||||
}
|
||||
|
||||
const res = await fetch( config.ajax.update_shipping.endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.ajax.update_shipping.nonce,
|
||||
order_id: data.orderID,
|
||||
} ),
|
||||
} );
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
|
||||
actions.reject();
|
||||
}
|
||||
};
|
||||
|
||||
handleShippingAddressChange = async ( data, actions ) => {
|
||||
try {
|
||||
const address = paypalAddressToWc(
|
||||
convertKeysToSnakeCase( data.shippingAddress )
|
||||
);
|
||||
|
||||
await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
|
||||
shipping_address: address,
|
||||
} );
|
||||
|
||||
await shippingData.setShippingAddress( address );
|
||||
|
||||
const res = await fetch( config.ajax.update_shipping.endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.ajax.update_shipping.nonce,
|
||||
order_id: data.orderID,
|
||||
} ),
|
||||
} );
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
|
||||
actions.reject();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect( () => {
|
||||
if ( activePaymentMethod !== methodId ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribeProcessing = onPaymentSetup( () => {
|
||||
if (
|
||||
cartHasSubscriptionProducts( config.scriptData ) &&
|
||||
config.scriptData.is_free_trial_cart
|
||||
) {
|
||||
return {
|
||||
type: responseTypes.SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
if ( config.scriptData.continuation ) {
|
||||
return {
|
||||
type: responseTypes.SUCCESS,
|
||||
meta: {
|
||||
paymentMethodData: {
|
||||
paypal_order_id:
|
||||
config.scriptData.continuation.order_id,
|
||||
funding_source:
|
||||
window.ppcpFundingSource ?? 'paypal',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const addresses = paypalOrderToWcAddresses( paypalOrder );
|
||||
|
||||
return {
|
||||
type: responseTypes.SUCCESS,
|
||||
meta: {
|
||||
paymentMethodData: {
|
||||
paypal_order_id: paypalOrder.id,
|
||||
funding_source: window.ppcpFundingSource ?? 'paypal',
|
||||
},
|
||||
...addresses,
|
||||
},
|
||||
};
|
||||
} );
|
||||
return () => {
|
||||
unsubscribeProcessing();
|
||||
};
|
||||
}, [ onPaymentSetup, paypalOrder, activePaymentMethod ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( activePaymentMethod !== methodId ) {
|
||||
return;
|
||||
}
|
||||
const unsubscribe = onCheckoutFail( ( { processingResponse } ) => {
|
||||
console.error( processingResponse );
|
||||
if ( onClose ) {
|
||||
onClose();
|
||||
}
|
||||
if ( config.scriptData.continuation ) {
|
||||
return true;
|
||||
}
|
||||
if ( shouldskipFinalConfirmation() ) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
}
|
||||
return true;
|
||||
} );
|
||||
return unsubscribe;
|
||||
}, [ onCheckoutFail, onClose, activePaymentMethod ] );
|
||||
|
||||
if ( config.scriptData.continuation ) {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: config.scriptData.continuation.cancel.html,
|
||||
} }
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! registeredContext ) {
|
||||
buttonModuleWatcher.registerContextBootstrap(
|
||||
config.scriptData.context,
|
||||
{
|
||||
createOrder: ( data ) => {
|
||||
return createOrder( data, config, onError, onClose );
|
||||
},
|
||||
onApprove: ( data, actions ) => {
|
||||
return handleApprove(
|
||||
data,
|
||||
actions,
|
||||
config,
|
||||
shouldHandleShippingInPayPal,
|
||||
shippingData,
|
||||
setPaypalOrder,
|
||||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
registeredContext = true;
|
||||
}
|
||||
|
||||
const style = normalizeStyleForFundingSource(
|
||||
config.scriptData.button.style,
|
||||
fundingSource
|
||||
);
|
||||
|
||||
if ( typeof buttonAttributes !== 'undefined' ) {
|
||||
style.height = buttonAttributes?.height
|
||||
? Number( buttonAttributes.height )
|
||||
: style.height;
|
||||
style.borderRadius = buttonAttributes?.borderRadius
|
||||
? Number( buttonAttributes.borderRadius )
|
||||
: style.borderRadius;
|
||||
}
|
||||
|
||||
if ( ! paypalScriptLoaded ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver(
|
||||
'react',
|
||||
{ React, ReactDOM }
|
||||
);
|
||||
|
||||
const getOnShippingOptionsChange = ( fundingSource ) => {
|
||||
if ( fundingSource === 'venmo' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ( data, actions ) => {
|
||||
shouldHandleShippingInPayPal()
|
||||
? handleShippingOptionsChange( data, actions )
|
||||
: null;
|
||||
};
|
||||
};
|
||||
|
||||
const getOnShippingAddressChange = ( fundingSource ) => {
|
||||
if ( fundingSource === 'venmo' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ( data, actions ) => {
|
||||
const shippingAddressChange = shouldHandleShippingInPayPal()
|
||||
? handleShippingAddressChange( data, actions )
|
||||
: null;
|
||||
|
||||
return shippingAddressChange;
|
||||
};
|
||||
};
|
||||
|
||||
if (
|
||||
cartHasSubscriptionProducts( config.scriptData ) &&
|
||||
config.scriptData.is_free_trial_cart
|
||||
) {
|
||||
return (
|
||||
<PayPalButton
|
||||
style={ style }
|
||||
onClick={ handleClick }
|
||||
onCancel={ onClose }
|
||||
onError={ onClose }
|
||||
createVaultSetupToken={ () => createVaultSetupToken( config ) }
|
||||
onApprove={ ( { vaultSetupToken } ) =>
|
||||
onApproveSavePayment( vaultSetupToken, config, onSubmit )
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ( isPayPalSubscription( config.scriptData ) ) {
|
||||
return (
|
||||
<PayPalButton
|
||||
fundingSource={ fundingSource }
|
||||
style={ style }
|
||||
onClick={ handleClick }
|
||||
onCancel={ onClose }
|
||||
onError={ onClose }
|
||||
createSubscription={ ( data, actions ) =>
|
||||
createSubscription( data, actions, config )
|
||||
}
|
||||
onApprove={ ( data, actions ) =>
|
||||
handleApproveSubscription(
|
||||
data,
|
||||
actions,
|
||||
config,
|
||||
shouldHandleShippingInPayPal,
|
||||
shippingData,
|
||||
setPaypalOrder,
|
||||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
)
|
||||
}
|
||||
onShippingOptionsChange={ getOnShippingOptionsChange(
|
||||
fundingSource
|
||||
) }
|
||||
onShippingAddressChange={ getOnShippingAddressChange(
|
||||
fundingSource
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PayPalButton
|
||||
fundingSource={ fundingSource }
|
||||
style={ style }
|
||||
onClick={ handleClick }
|
||||
onCancel={ onClose }
|
||||
onError={ onClose }
|
||||
createOrder={ ( data ) =>
|
||||
createOrder( data, config, onError, onClose )
|
||||
}
|
||||
onApprove={ ( data, actions ) =>
|
||||
handleApprove(
|
||||
data,
|
||||
actions,
|
||||
config,
|
||||
shouldHandleShippingInPayPal,
|
||||
shippingData,
|
||||
setPaypalOrder,
|
||||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
)
|
||||
}
|
||||
onShippingOptionsChange={ getOnShippingOptionsChange(
|
||||
fundingSource
|
||||
) }
|
||||
onShippingAddressChange={ getOnShippingAddressChange(
|
||||
fundingSource
|
||||
) }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,750 +1,26 @@
|
|||
import { useEffect, useState, useMemo } from '@wordpress/element';
|
||||
import {
|
||||
registerExpressPaymentMethod,
|
||||
registerPaymentMethod,
|
||||
} from '@woocommerce/blocks-registry';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
mergeWcAddress,
|
||||
paypalAddressToWc,
|
||||
paypalOrderToWcAddresses,
|
||||
paypalSubscriptionToWcAddresses,
|
||||
} from './Helper/Address';
|
||||
import { convertKeysToSnakeCase } from './Helper/Helper';
|
||||
import {
|
||||
cartHasSubscriptionProducts,
|
||||
isPayPalSubscription,
|
||||
} from './Helper/Subscription';
|
||||
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
|
||||
import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
|
||||
import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style';
|
||||
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
|
||||
import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap';
|
||||
import { PayPalComponent } from './Components/paypal';
|
||||
import { BlockEditorPayPalComponent } from './Components/block-editor-paypal';
|
||||
import { PaypalLabel } from './Components/paypal-label';
|
||||
const namespace = 'ppcpBlocksPaypalExpressButtons';
|
||||
const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' );
|
||||
|
||||
window.ppcpFundingSource = config.fundingSource;
|
||||
|
||||
let registeredContext = false;
|
||||
let paypalScriptPromise = null;
|
||||
|
||||
const PAYPAL_GATEWAY_ID = 'ppcp-gateway';
|
||||
|
||||
const PayPalComponent = ( {
|
||||
onClick,
|
||||
onClose,
|
||||
onSubmit,
|
||||
onError,
|
||||
eventRegistration,
|
||||
emitResponse,
|
||||
activePaymentMethod,
|
||||
shippingData,
|
||||
isEditing,
|
||||
fundingSource,
|
||||
buttonAttributes,
|
||||
} ) => {
|
||||
const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } =
|
||||
eventRegistration;
|
||||
const { responseTypes } = emitResponse;
|
||||
|
||||
const [ paypalOrder, setPaypalOrder ] = useState( null );
|
||||
const [ continuationFilled, setContinuationFilled ] = useState( false );
|
||||
const [ gotoContinuationOnError, setGotoContinuationOnError ] =
|
||||
useState( false );
|
||||
|
||||
const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false );
|
||||
|
||||
if ( ! paypalScriptLoaded ) {
|
||||
if ( ! paypalScriptPromise ) {
|
||||
// for editor, since canMakePayment was not called
|
||||
paypalScriptPromise = loadPayPalScript(
|
||||
namespace,
|
||||
config.scriptData
|
||||
);
|
||||
}
|
||||
paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) );
|
||||
}
|
||||
|
||||
const methodId = fundingSource
|
||||
? `${ config.id }-${ fundingSource }`
|
||||
: config.id;
|
||||
|
||||
/**
|
||||
* The block cart displays express checkout buttons. Those buttons are handled by the
|
||||
* PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons").
|
||||
*
|
||||
* A possible bug in WooCommerce does not use the correct payment method ID for the express
|
||||
* payment buttons inside the cart, but sends the ID of the _first_ active payment method.
|
||||
*
|
||||
* This function uses an internal WooCommerce dispatcher method to set the correct method ID.
|
||||
*/
|
||||
const enforcePaymentMethodForCart = () => {
|
||||
// Do nothing, unless we're handling block cart express payment buttons.
|
||||
if ( 'cart-block' !== config.scriptData.context ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the active payment method to PAYPAL_GATEWAY_ID.
|
||||
wp.data
|
||||
.dispatch( 'wc/store/payment' )
|
||||
.__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} );
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
// fill the form if in continuation (for product or mini-cart buttons)
|
||||
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const paypalAddresses = paypalOrderToWcAddresses(
|
||||
config.scriptData.continuation.order
|
||||
);
|
||||
const wcAddresses = wp.data
|
||||
.select( 'wc/store/cart' )
|
||||
.getCustomerData();
|
||||
const addresses = mergeWcAddress( wcAddresses, paypalAddresses );
|
||||
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setBillingAddress( addresses.billingAddress );
|
||||
|
||||
if ( shippingData.needsShipping ) {
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setShippingAddress( addresses.shippingAddress );
|
||||
}
|
||||
} catch ( err ) {
|
||||
// sometimes the PayPal address is missing, skip in this case.
|
||||
console.log( err );
|
||||
}
|
||||
|
||||
// this useEffect should run only once, but adding this in case of some kind of full re-rendering
|
||||
setContinuationFilled( true );
|
||||
}, [ shippingData, continuationFilled ] );
|
||||
|
||||
const createOrder = async ( data, actions ) => {
|
||||
try {
|
||||
const requestBody = {
|
||||
nonce: config.scriptData.ajax.create_order.nonce,
|
||||
bn_code: '',
|
||||
context: config.scriptData.context,
|
||||
payment_method: 'ppcp-gateway',
|
||||
funding_source: window.ppcpFundingSource ?? 'paypal',
|
||||
createaccount: false,
|
||||
...( data?.paymentSource && {
|
||||
payment_source: data.paymentSource,
|
||||
} ),
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
config.scriptData.ajax.create_order.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( requestBody ),
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
if ( json.data?.details?.length > 0 ) {
|
||||
throw new Error(
|
||||
json.data.details
|
||||
.map( ( d ) => `${ d.issue } ${ d.description }` )
|
||||
.join( '<br/>' )
|
||||
);
|
||||
} else if ( json.data?.message ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
|
||||
throw new Error( config.scriptData.labels.error.generic );
|
||||
}
|
||||
return json.data.id;
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
|
||||
onError( err.message );
|
||||
|
||||
onClose();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const createSubscription = async ( data, actions ) => {
|
||||
let planId = config.scriptData.subscription_plan_id;
|
||||
if (
|
||||
config.scriptData
|
||||
.variable_paypal_subscription_variation_from_cart !== ''
|
||||
) {
|
||||
planId =
|
||||
config.scriptData
|
||||
.variable_paypal_subscription_variation_from_cart;
|
||||
}
|
||||
|
||||
return actions.subscription.create( {
|
||||
plan_id: planId,
|
||||
} );
|
||||
};
|
||||
|
||||
const handleApproveSubscription = async ( data, actions ) => {
|
||||
try {
|
||||
const subscription = await actions.subscription.get();
|
||||
|
||||
if ( subscription ) {
|
||||
const addresses =
|
||||
paypalSubscriptionToWcAddresses( subscription );
|
||||
|
||||
const promises = [
|
||||
// save address on server
|
||||
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
|
||||
billing_address: addresses.billingAddress,
|
||||
shipping_address: addresses.shippingAddress,
|
||||
} ),
|
||||
];
|
||||
if ( shouldHandleShippingInPayPal() ) {
|
||||
// set address in UI
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setBillingAddress( addresses.billingAddress )
|
||||
);
|
||||
if ( shippingData.needsShipping ) {
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setShippingAddress( addresses.shippingAddress )
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all( promises );
|
||||
}
|
||||
|
||||
setPaypalOrder( subscription );
|
||||
|
||||
const res = await fetch(
|
||||
config.scriptData.ajax.approve_subscription.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.scriptData.ajax.approve_subscription
|
||||
.nonce,
|
||||
order_id: data.orderID,
|
||||
subscription_id: data.subscriptionID,
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
if (
|
||||
typeof actions !== 'undefined' &&
|
||||
typeof actions.restart !== 'undefined'
|
||||
) {
|
||||
return actions.restart();
|
||||
}
|
||||
if ( json.data?.message ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
|
||||
throw new Error( config.scriptData.labels.error.generic );
|
||||
}
|
||||
|
||||
if ( ! shouldskipFinalConfirmation() ) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
} else {
|
||||
setGotoContinuationOnError( true );
|
||||
enforcePaymentMethodForCart();
|
||||
onSubmit();
|
||||
}
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
|
||||
onError( err.message );
|
||||
|
||||
onClose();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getCheckoutRedirectUrl = () => {
|
||||
const checkoutUrl = new URL( config.scriptData.redirect );
|
||||
// sometimes some browsers may load some kind of cached version of the page,
|
||||
// so adding a parameter to avoid that
|
||||
checkoutUrl.searchParams.append(
|
||||
'ppcp-continuation-redirect',
|
||||
new Date().getTime().toString()
|
||||
);
|
||||
return checkoutUrl.toString();
|
||||
};
|
||||
|
||||
const handleApprove = async ( data, actions ) => {
|
||||
try {
|
||||
const order = await actions.order.get();
|
||||
|
||||
if ( order ) {
|
||||
const addresses = paypalOrderToWcAddresses( order );
|
||||
|
||||
const promises = [
|
||||
// save address on server
|
||||
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
|
||||
billing_address: addresses.billingAddress,
|
||||
shipping_address: addresses.shippingAddress,
|
||||
} ),
|
||||
];
|
||||
if ( shouldHandleShippingInPayPal() ) {
|
||||
// set address in UI
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setBillingAddress( addresses.billingAddress )
|
||||
);
|
||||
if ( shippingData.needsShipping ) {
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setShippingAddress( addresses.shippingAddress )
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all( promises );
|
||||
}
|
||||
|
||||
setPaypalOrder( order );
|
||||
|
||||
const res = await fetch(
|
||||
config.scriptData.ajax.approve_order.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.scriptData.ajax.approve_order.nonce,
|
||||
order_id: data.orderID,
|
||||
funding_source: window.ppcpFundingSource ?? 'paypal',
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
if (
|
||||
typeof actions !== 'undefined' &&
|
||||
typeof actions.restart !== 'undefined'
|
||||
) {
|
||||
return actions.restart();
|
||||
}
|
||||
if ( json.data?.message ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
|
||||
throw new Error( config.scriptData.labels.error.generic );
|
||||
}
|
||||
|
||||
if ( ! shouldskipFinalConfirmation() ) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
} else {
|
||||
setGotoContinuationOnError( true );
|
||||
enforcePaymentMethodForCart();
|
||||
onSubmit();
|
||||
}
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
|
||||
onError( err.message );
|
||||
|
||||
onClose();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
const unsubscribe = onCheckoutValidation( () => {
|
||||
if ( config.scriptData.continuation ) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
gotoContinuationOnError &&
|
||||
wp.data.select( 'wc/store/validation' ).hasValidationErrors()
|
||||
) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
return { type: responseTypes.ERROR };
|
||||
}
|
||||
|
||||
return true;
|
||||
} );
|
||||
return unsubscribe;
|
||||
}, [ onCheckoutValidation, gotoContinuationOnError ] );
|
||||
|
||||
const handleClick = ( data, actions ) => {
|
||||
if ( isEditing ) {
|
||||
return actions.reject();
|
||||
}
|
||||
|
||||
window.ppcpFundingSource = data.fundingSource;
|
||||
|
||||
onClick();
|
||||
};
|
||||
|
||||
const shouldHandleShippingInPayPal = () => {
|
||||
return shouldskipFinalConfirmation() && config.needShipping;
|
||||
};
|
||||
|
||||
const shouldskipFinalConfirmation = () => {
|
||||
if ( config.finalReviewEnabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
window.ppcpFundingSource !== 'venmo' ||
|
||||
! config.scriptData.vaultingEnabled
|
||||
);
|
||||
};
|
||||
|
||||
let handleShippingOptionsChange = null;
|
||||
let handleShippingAddressChange = null;
|
||||
let handleSubscriptionShippingOptionsChange = null;
|
||||
let handleSubscriptionShippingAddressChange = null;
|
||||
|
||||
if ( shippingData.needsShipping && shouldHandleShippingInPayPal() ) {
|
||||
handleShippingOptionsChange = async ( data, actions ) => {
|
||||
try {
|
||||
const shippingOptionId = data.selectedShippingOption?.id;
|
||||
if ( shippingOptionId ) {
|
||||
await wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.selectShippingRate( shippingOptionId );
|
||||
await shippingData.setSelectedRates( shippingOptionId );
|
||||
}
|
||||
|
||||
const res = await fetch( config.ajax.update_shipping.endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.ajax.update_shipping.nonce,
|
||||
order_id: data.orderID,
|
||||
} ),
|
||||
} );
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
|
||||
actions.reject();
|
||||
}
|
||||
};
|
||||
|
||||
handleShippingAddressChange = async ( data, actions ) => {
|
||||
try {
|
||||
const address = paypalAddressToWc(
|
||||
convertKeysToSnakeCase( data.shippingAddress )
|
||||
);
|
||||
|
||||
await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
|
||||
shipping_address: address,
|
||||
} );
|
||||
|
||||
await shippingData.setShippingAddress( address );
|
||||
|
||||
const res = await fetch( config.ajax.update_shipping.endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.ajax.update_shipping.nonce,
|
||||
order_id: data.orderID,
|
||||
} ),
|
||||
} );
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
|
||||
actions.reject();
|
||||
}
|
||||
};
|
||||
|
||||
handleSubscriptionShippingOptionsChange = async ( data, actions ) => {
|
||||
try {
|
||||
const shippingOptionId = data.selectedShippingOption?.id;
|
||||
if ( shippingOptionId ) {
|
||||
await wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.selectShippingRate( shippingOptionId );
|
||||
await shippingData.setSelectedRates( shippingOptionId );
|
||||
}
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
|
||||
actions.reject();
|
||||
}
|
||||
};
|
||||
|
||||
handleSubscriptionShippingAddressChange = async ( data, actions ) => {
|
||||
try {
|
||||
const address = paypalAddressToWc(
|
||||
convertKeysToSnakeCase( data.shippingAddress )
|
||||
);
|
||||
|
||||
await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
|
||||
shipping_address: address,
|
||||
} );
|
||||
|
||||
await shippingData.setShippingAddress( address );
|
||||
|
||||
const res = await fetch( config.ajax.update_shipping.endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.ajax.update_shipping.nonce,
|
||||
order_id: data.orderID,
|
||||
} ),
|
||||
} );
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
|
||||
actions.reject();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect( () => {
|
||||
if ( activePaymentMethod !== methodId ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribeProcessing = onPaymentSetup( () => {
|
||||
if ( config.scriptData.continuation ) {
|
||||
return {
|
||||
type: responseTypes.SUCCESS,
|
||||
meta: {
|
||||
paymentMethodData: {
|
||||
paypal_order_id:
|
||||
config.scriptData.continuation.order_id,
|
||||
funding_source:
|
||||
window.ppcpFundingSource ?? 'paypal',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const addresses = paypalOrderToWcAddresses( paypalOrder );
|
||||
|
||||
return {
|
||||
type: responseTypes.SUCCESS,
|
||||
meta: {
|
||||
paymentMethodData: {
|
||||
paypal_order_id: paypalOrder.id,
|
||||
funding_source: window.ppcpFundingSource ?? 'paypal',
|
||||
},
|
||||
...addresses,
|
||||
},
|
||||
};
|
||||
} );
|
||||
return () => {
|
||||
unsubscribeProcessing();
|
||||
};
|
||||
}, [ onPaymentSetup, paypalOrder, activePaymentMethod ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( activePaymentMethod !== methodId ) {
|
||||
return;
|
||||
}
|
||||
const unsubscribe = onCheckoutFail( ( { processingResponse } ) => {
|
||||
console.error( processingResponse );
|
||||
if ( onClose ) {
|
||||
onClose();
|
||||
}
|
||||
if ( config.scriptData.continuation ) {
|
||||
return true;
|
||||
}
|
||||
if ( shouldskipFinalConfirmation() ) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
}
|
||||
return true;
|
||||
} );
|
||||
return unsubscribe;
|
||||
}, [ onCheckoutFail, onClose, activePaymentMethod ] );
|
||||
|
||||
if ( config.scriptData.continuation ) {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: config.scriptData.continuation.cancel.html,
|
||||
} }
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! registeredContext ) {
|
||||
buttonModuleWatcher.registerContextBootstrap(
|
||||
config.scriptData.context,
|
||||
{
|
||||
createOrder: () => {
|
||||
return createOrder();
|
||||
},
|
||||
onApprove: ( data, actions ) => {
|
||||
return handleApprove( data, actions );
|
||||
},
|
||||
}
|
||||
);
|
||||
registeredContext = true;
|
||||
}
|
||||
|
||||
const style = normalizeStyleForFundingSource(
|
||||
config.scriptData.button.style,
|
||||
fundingSource
|
||||
);
|
||||
|
||||
if ( typeof buttonAttributes !== 'undefined' ) {
|
||||
style.height = buttonAttributes?.height
|
||||
? Number( buttonAttributes.height )
|
||||
: style.height;
|
||||
style.borderRadius = buttonAttributes?.borderRadius
|
||||
? Number( buttonAttributes.borderRadius )
|
||||
: style.borderRadius;
|
||||
}
|
||||
|
||||
if ( ! paypalScriptLoaded ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver(
|
||||
'react',
|
||||
{ React, ReactDOM }
|
||||
);
|
||||
|
||||
const getOnShippingOptionsChange = ( fundingSource ) => {
|
||||
if ( fundingSource === 'venmo' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ( data, actions ) => {
|
||||
shouldHandleShippingInPayPal()
|
||||
? handleShippingOptionsChange( data, actions )
|
||||
: null;
|
||||
};
|
||||
};
|
||||
|
||||
const getOnShippingAddressChange = ( fundingSource ) => {
|
||||
if ( fundingSource === 'venmo' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ( data, actions ) => {
|
||||
const shippingAddressChange = shouldHandleShippingInPayPal()
|
||||
? handleShippingAddressChange( data, actions )
|
||||
: null;
|
||||
|
||||
return shippingAddressChange;
|
||||
};
|
||||
};
|
||||
|
||||
if ( isPayPalSubscription( config.scriptData ) ) {
|
||||
return (
|
||||
<PayPalButton
|
||||
fundingSource={ fundingSource }
|
||||
style={ style }
|
||||
onClick={ handleClick }
|
||||
onCancel={ onClose }
|
||||
onError={ onClose }
|
||||
createSubscription={ createSubscription }
|
||||
onApprove={ handleApproveSubscription }
|
||||
onShippingOptionsChange={ getOnShippingOptionsChange(
|
||||
fundingSource
|
||||
) }
|
||||
onShippingAddressChange={ getOnShippingAddressChange(
|
||||
fundingSource
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PayPalButton
|
||||
fundingSource={ fundingSource }
|
||||
style={ style }
|
||||
onClick={ handleClick }
|
||||
onCancel={ onClose }
|
||||
onError={ onClose }
|
||||
createOrder={ createOrder }
|
||||
onApprove={ handleApprove }
|
||||
onShippingOptionsChange={ getOnShippingOptionsChange(
|
||||
fundingSource
|
||||
) }
|
||||
onShippingAddressChange={ getOnShippingAddressChange(
|
||||
fundingSource
|
||||
) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockEditorPayPalComponent = ( { fundingSource, buttonAttributes } ) => {
|
||||
const urlParams = useMemo(
|
||||
() => ( {
|
||||
clientId: 'test',
|
||||
...config.scriptData.url_params,
|
||||
dataNamespace: 'ppcp-blocks-editor-paypal-buttons',
|
||||
components: 'buttons',
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
const style = useMemo( () => {
|
||||
const configStyle = normalizeStyleForFundingSource(
|
||||
config.scriptData.button.style,
|
||||
fundingSource
|
||||
);
|
||||
|
||||
if ( buttonAttributes ) {
|
||||
return {
|
||||
...configStyle,
|
||||
height: buttonAttributes.height
|
||||
? Number( buttonAttributes.height )
|
||||
: configStyle.height,
|
||||
borderRadius: buttonAttributes.borderRadius
|
||||
? Number( buttonAttributes.borderRadius )
|
||||
: configStyle.borderRadius,
|
||||
};
|
||||
}
|
||||
|
||||
return configStyle;
|
||||
}, [ fundingSource, buttonAttributes ] );
|
||||
|
||||
return (
|
||||
<PayPalScriptProvider options={ urlParams }>
|
||||
<PayPalButtons
|
||||
className={ `ppc-button-container-${ fundingSource }` }
|
||||
fundingSource={ fundingSource }
|
||||
style={ style }
|
||||
forceReRender={ [ buttonAttributes || {} ] }
|
||||
onClick={ () => false }
|
||||
/>
|
||||
</PayPalScriptProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const features = [ 'products' ];
|
||||
let block_enabled = true;
|
||||
let blockEnabled = true;
|
||||
|
||||
if ( cartHasSubscriptionProducts( config.scriptData ) ) {
|
||||
// Don't show buttons on block cart page if using vault v2 and user is not logged in
|
||||
|
@ -754,7 +30,17 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
|
|||
! isPayPalSubscription( config.scriptData ) && // using vaulting
|
||||
! config.scriptData?.save_payment_methods?.id_token // not vault v3
|
||||
) {
|
||||
block_enabled = false;
|
||||
blockEnabled = false;
|
||||
}
|
||||
|
||||
// Don't show buttons on block cart page if user is not logged in and cart contains free trial product
|
||||
if (
|
||||
! config.scriptData.user.is_logged &&
|
||||
config.scriptData.context === 'cart-block' &&
|
||||
cartHasSubscriptionProducts( config.scriptData ) &&
|
||||
config.scriptData.is_free_trial_cart
|
||||
) {
|
||||
blockEnabled = false;
|
||||
}
|
||||
|
||||
// Don't render if vaulting disabled and is in vault subscription mode
|
||||
|
@ -762,7 +48,7 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
|
|||
! isPayPalSubscription( config.scriptData ) &&
|
||||
! config.scriptData.can_save_vault_token
|
||||
) {
|
||||
block_enabled = false;
|
||||
blockEnabled = false;
|
||||
}
|
||||
|
||||
// Don't render buttons if in subscription mode and product not associated with a PayPal subscription
|
||||
|
@ -770,13 +56,21 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
|
|||
isPayPalSubscription( config.scriptData ) &&
|
||||
! config.scriptData.subscription_product_allowed
|
||||
) {
|
||||
block_enabled = false;
|
||||
blockEnabled = false;
|
||||
}
|
||||
|
||||
// Don't show buttons if cart contains free trial product and the stroe is not eligible for saving payment methods.
|
||||
if (
|
||||
! config.scriptData.vault_v3_enabled &&
|
||||
config.scriptData.is_free_trial_cart
|
||||
) {
|
||||
blockEnabled = false;
|
||||
}
|
||||
|
||||
features.push( 'subscriptions' );
|
||||
}
|
||||
|
||||
if ( block_enabled ) {
|
||||
if ( blockEnabled ) {
|
||||
if ( config.placeOrderEnabled && ! config.scriptData.continuation ) {
|
||||
let descriptionElement = (
|
||||
<div
|
||||
|
@ -802,21 +96,6 @@ if ( block_enabled ) {
|
|||
);
|
||||
}
|
||||
|
||||
const PaypalLabel = ( { components, config } ) => {
|
||||
const { PaymentMethodIcons } = components;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: config.title,
|
||||
} }
|
||||
/>
|
||||
<PaymentMethodIcons icons={ config.icon } align="right" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
registerPaymentMethod( {
|
||||
name: config.id,
|
||||
label: <PaypalLabel config={ config } />,
|
||||
|
@ -837,8 +116,13 @@ if ( block_enabled ) {
|
|||
registerPaymentMethod( {
|
||||
name: config.id,
|
||||
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
|
||||
content: <PayPalComponent isEditing={ false } />,
|
||||
edit: <BlockEditorPayPalComponent fundingSource={ 'paypal' } />,
|
||||
content: <PayPalComponent config={ config } isEditing={ false } />,
|
||||
edit: (
|
||||
<BlockEditorPayPalComponent
|
||||
config={ config }
|
||||
fundingSource={ 'paypal' }
|
||||
/>
|
||||
),
|
||||
ariaLabel: config.title,
|
||||
canMakePayment: () => {
|
||||
return true;
|
||||
|
@ -848,10 +132,11 @@ if ( block_enabled ) {
|
|||
},
|
||||
} );
|
||||
} else if ( config.smartButtonsEnabled ) {
|
||||
for ( const fundingSource of [
|
||||
'paypal',
|
||||
...config.enabledFundingSources,
|
||||
] ) {
|
||||
const fundingSources = config.scriptData.is_free_trial_cart
|
||||
? [ 'paypal' ]
|
||||
: [ 'paypal', ...config.enabledFundingSources ];
|
||||
|
||||
for ( const fundingSource of fundingSources ) {
|
||||
registerExpressPaymentMethod( {
|
||||
name: `${ config.id }-${ fundingSource }`,
|
||||
title: 'PayPal',
|
||||
|
@ -866,12 +151,14 @@ if ( block_enabled ) {
|
|||
),
|
||||
content: (
|
||||
<PayPalComponent
|
||||
config={ config }
|
||||
isEditing={ false }
|
||||
fundingSource={ fundingSource }
|
||||
/>
|
||||
),
|
||||
edit: (
|
||||
<BlockEditorPayPalComponent
|
||||
config={ config }
|
||||
fundingSource={ fundingSource }
|
||||
/>
|
||||
),
|
||||
|
|
316
modules/ppcp-blocks/resources/js/paypal-config.js
Normal file
316
modules/ppcp-blocks/resources/js/paypal-config.js
Normal file
|
@ -0,0 +1,316 @@
|
|||
import {
|
||||
paypalOrderToWcAddresses,
|
||||
paypalSubscriptionToWcAddresses,
|
||||
} from './Helper/Address';
|
||||
|
||||
export const createOrder = async ( data, config, onError, onClose ) => {
|
||||
try {
|
||||
const requestBody = {
|
||||
nonce: config.scriptData.ajax.create_order.nonce,
|
||||
bn_code: '',
|
||||
context: config.scriptData.context,
|
||||
payment_method: 'ppcp-gateway',
|
||||
funding_source: window.ppcpFundingSource ?? 'paypal',
|
||||
createaccount: false,
|
||||
...( data?.paymentSource && {
|
||||
payment_source: data.paymentSource,
|
||||
} ),
|
||||
};
|
||||
|
||||
const res = await fetch( config.scriptData.ajax.create_order.endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( requestBody ),
|
||||
} );
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
if ( json.data?.details?.length > 0 ) {
|
||||
throw new Error(
|
||||
json.data.details
|
||||
.map( ( d ) => `${ d.issue } ${ d.description }` )
|
||||
.join( '<br/>' )
|
||||
);
|
||||
} else if ( json.data?.message ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
|
||||
throw new Error( config.scriptData.labels.error.generic );
|
||||
}
|
||||
|
||||
return json.data.id;
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
|
||||
onError( err.message );
|
||||
|
||||
onClose();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const handleApprove = async (
|
||||
data,
|
||||
actions,
|
||||
config,
|
||||
shouldHandleShippingInPayPal,
|
||||
shippingData,
|
||||
setPaypalOrder,
|
||||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
) => {
|
||||
try {
|
||||
const order = await actions.order.get();
|
||||
|
||||
if ( order ) {
|
||||
const addresses = paypalOrderToWcAddresses( order );
|
||||
|
||||
const promises = [
|
||||
// save address on server
|
||||
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
|
||||
billing_address: addresses.billingAddress,
|
||||
shipping_address: addresses.shippingAddress,
|
||||
} ),
|
||||
];
|
||||
if ( shouldHandleShippingInPayPal() ) {
|
||||
// set address in UI
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setBillingAddress( addresses.billingAddress )
|
||||
);
|
||||
if ( shippingData.needsShipping ) {
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setShippingAddress( addresses.shippingAddress )
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all( promises );
|
||||
}
|
||||
|
||||
setPaypalOrder( order );
|
||||
|
||||
const res = await fetch(
|
||||
config.scriptData.ajax.approve_order.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.scriptData.ajax.approve_order.nonce,
|
||||
order_id: data.orderID,
|
||||
funding_source: window.ppcpFundingSource ?? 'paypal',
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
if (
|
||||
typeof actions !== 'undefined' &&
|
||||
typeof actions.restart !== 'undefined'
|
||||
) {
|
||||
return actions.restart();
|
||||
}
|
||||
if ( json.data?.message ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
|
||||
throw new Error( config.scriptData.labels.error.generic );
|
||||
}
|
||||
|
||||
if ( ! shouldskipFinalConfirmation() ) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
} else {
|
||||
setGotoContinuationOnError( true );
|
||||
enforcePaymentMethodForCart();
|
||||
onSubmit();
|
||||
}
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
|
||||
onError( err.message );
|
||||
|
||||
onClose();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const createSubscription = async ( data, actions, config ) => {
|
||||
let planId = config.scriptData.subscription_plan_id;
|
||||
if (
|
||||
config.scriptData.variable_paypal_subscription_variation_from_cart !==
|
||||
''
|
||||
) {
|
||||
planId =
|
||||
config.scriptData.variable_paypal_subscription_variation_from_cart;
|
||||
}
|
||||
|
||||
return actions.subscription.create( {
|
||||
plan_id: planId,
|
||||
} );
|
||||
};
|
||||
|
||||
export const handleApproveSubscription = async (
|
||||
data,
|
||||
actions,
|
||||
config,
|
||||
shouldHandleShippingInPayPal,
|
||||
shippingData,
|
||||
setPaypalOrder,
|
||||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
) => {
|
||||
try {
|
||||
const subscription = await actions.subscription.get();
|
||||
|
||||
if ( subscription ) {
|
||||
const addresses = paypalSubscriptionToWcAddresses( subscription );
|
||||
|
||||
const promises = [
|
||||
// save address on server
|
||||
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
|
||||
billing_address: addresses.billingAddress,
|
||||
shipping_address: addresses.shippingAddress,
|
||||
} ),
|
||||
];
|
||||
if ( shouldHandleShippingInPayPal() ) {
|
||||
// set address in UI
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setBillingAddress( addresses.billingAddress )
|
||||
);
|
||||
if ( shippingData.needsShipping ) {
|
||||
promises.push(
|
||||
wp.data
|
||||
.dispatch( 'wc/store/cart' )
|
||||
.setShippingAddress( addresses.shippingAddress )
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all( promises );
|
||||
}
|
||||
|
||||
setPaypalOrder( subscription );
|
||||
|
||||
const res = await fetch(
|
||||
config.scriptData.ajax.approve_subscription.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: config.scriptData.ajax.approve_subscription.nonce,
|
||||
order_id: data.orderID,
|
||||
subscription_id: data.subscriptionID,
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if ( ! json.success ) {
|
||||
if (
|
||||
typeof actions !== 'undefined' &&
|
||||
typeof actions.restart !== 'undefined'
|
||||
) {
|
||||
return actions.restart();
|
||||
}
|
||||
if ( json.data?.message ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
|
||||
throw new Error( config.scriptData.labels.error.generic );
|
||||
}
|
||||
|
||||
if ( ! shouldskipFinalConfirmation() ) {
|
||||
location.href = getCheckoutRedirectUrl();
|
||||
} else {
|
||||
setGotoContinuationOnError( true );
|
||||
enforcePaymentMethodForCart();
|
||||
onSubmit();
|
||||
}
|
||||
} catch ( err ) {
|
||||
console.error( err );
|
||||
|
||||
onError( err.message );
|
||||
|
||||
onClose();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const createVaultSetupToken = async ( config ) => {
|
||||
return fetch( config.scriptData.ajax.create_setup_token.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
nonce: config.scriptData.ajax.create_setup_token.nonce,
|
||||
payment_method: 'ppcp-gateway',
|
||||
} ),
|
||||
} )
|
||||
.then( ( response ) => response.json() )
|
||||
.then( ( result ) => {
|
||||
return result.data.id;
|
||||
} )
|
||||
.catch( ( err ) => {
|
||||
console.error( err );
|
||||
} );
|
||||
};
|
||||
|
||||
export const onApproveSavePayment = async (
|
||||
vaultSetupToken,
|
||||
config,
|
||||
onSubmit
|
||||
) => {
|
||||
let endpoint =
|
||||
config.scriptData.ajax.create_payment_token_for_guest.endpoint;
|
||||
let bodyContent = {
|
||||
nonce: config.scriptData.ajax.create_payment_token_for_guest.nonce,
|
||||
vault_setup_token: vaultSetupToken,
|
||||
};
|
||||
|
||||
if ( config.scriptData.user.is_logged_in ) {
|
||||
endpoint = config.scriptData.ajax.create_payment_token.endpoint;
|
||||
|
||||
bodyContent = {
|
||||
nonce: config.scriptData.ajax.create_payment_token.nonce,
|
||||
vault_setup_token: vaultSetupToken,
|
||||
is_free_trial_cart: config.scriptData.is_free_trial_cart,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch( endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify( bodyContent ),
|
||||
} );
|
||||
|
||||
const result = await response.json();
|
||||
if ( result.success === true ) {
|
||||
onSubmit();
|
||||
}
|
||||
|
||||
console.error( result );
|
||||
};
|
|
@ -96,7 +96,7 @@ class CreatePaymentToken implements EndpointInterface {
|
|||
|
||||
$customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
|
||||
|
||||
$result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source, $customer_id );
|
||||
$result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source, (string) $customer_id );
|
||||
|
||||
if ( is_user_logged_in() && isset( $result->customer->id ) ) {
|
||||
$current_user_id = get_current_user_id();
|
||||
|
|
|
@ -105,7 +105,7 @@ class CreateSetupToken implements EndpointInterface {
|
|||
|
||||
$customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
|
||||
|
||||
$result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source, $customer_id );
|
||||
$result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source, (string) $customer_id );
|
||||
|
||||
wp_send_json_success( $result );
|
||||
return true;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue