Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
woocommerce-paypal-payments
/
modules
/
ppcp-blocks
/
resources
/
js
:
checkout-block.js
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
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'; 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; if ( cartHasSubscriptionProducts( config.scriptData ) ) { // Don't show buttons on block cart page if using vault v2 and user is not logged in if ( ! config.scriptData.user.is_logged && config.scriptData.context === 'cart-block' && ! isPayPalSubscription( config.scriptData ) && // using vaulting ! config.scriptData?.save_payment_methods?.id_token // not vault v3 ) { block_enabled = false; } // Don't render if vaulting disabled and is in vault subscription mode if ( ! isPayPalSubscription( config.scriptData ) && ! config.scriptData.can_save_vault_token ) { block_enabled = false; } // Don't render buttons if in subscription mode and product not associated with a PayPal subscription if ( isPayPalSubscription( config.scriptData ) && ! config.scriptData.subscription_product_allowed ) { block_enabled = false; } features.push( 'subscriptions' ); } if ( block_enabled ) { if ( config.placeOrderEnabled && ! config.scriptData.continuation ) { let descriptionElement = ( <div dangerouslySetInnerHTML={ { __html: config.description } } ></div> ); if ( config.placeOrderButtonDescription ) { descriptionElement = ( <div> <p dangerouslySetInnerHTML={ { __html: config.description, } } ></p> <p style={ { textAlign: 'center' } } className={ 'ppcp-place-order-description' } dangerouslySetInnerHTML={ { __html: config.placeOrderButtonDescription, } } ></p> </div> ); } registerPaymentMethod( { name: config.id, label: <div dangerouslySetInnerHTML={ { __html: config.title } } />, content: descriptionElement, edit: descriptionElement, placeOrderButtonLabel: config.placeOrderButtonText, ariaLabel: config.title, canMakePayment: () => { return true; }, supports: { features, }, } ); } if ( config.scriptData.continuation ) { registerPaymentMethod( { name: config.id, label: <div dangerouslySetInnerHTML={ { __html: config.title } } />, content: <PayPalComponent isEditing={ false } />, edit: <BlockEditorPayPalComponent fundingSource={ 'paypal' } />, ariaLabel: config.title, canMakePayment: () => { return true; }, supports: { features: [ ...features, 'ppcp_continuation' ], }, } ); } else if ( config.smartButtonsEnabled ) { for ( const fundingSource of [ 'paypal', ...config.enabledFundingSources, ] ) { registerExpressPaymentMethod( { name: `${ config.id }-${ fundingSource }`, title: 'PayPal', description: __( 'Eligible users will see the PayPal button.', 'woocommerce-paypal-payments' ), gatewayId: 'ppcp-gateway', paymentMethodId: config.id, label: ( <div dangerouslySetInnerHTML={ { __html: config.title } } /> ), content: ( <PayPalComponent isEditing={ false } fundingSource={ fundingSource } /> ), edit: ( <BlockEditorPayPalComponent fundingSource={ fundingSource } /> ), ariaLabel: config.title, canMakePayment: async () => { if ( ! paypalScriptPromise ) { paypalScriptPromise = loadPayPalScript( namespace, config.scriptData ); paypalScriptPromise.then( () => { const messagesBootstrap = new BlockCheckoutMessagesBootstrap( config.scriptData ); messagesBootstrap.init(); } ); } await paypalScriptPromise; return ppcpBlocksPaypalExpressButtons .Buttons( { fundingSource } ) .isEligible(); }, supports: { features, style: [ 'height', 'borderRadius' ], }, } ); } } }