File "checkout-block.js"

Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-blocks/resources/js/checkout-block.js
File size: 21.77 KB
MIME-type: text/x-java
Charset: utf-8

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' ],
				},
			} );
		}
	}
}