import {useEffect, useState} from '@wordpress/element'; import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; import { mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses, paypalSubscriptionToWcAddresses } from "./Helper/Address"; import { convertKeysToSnakeCase } from "./Helper/Helper"; import { cartHasSubscriptionProducts, isPayPalSubscription } from "./Helper/Subscription"; import { loadPaypalScriptPromise } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' 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 {keysToCamelCase} from "../../../ppcp-button/resources/js/modules/Helper/Utils"; import { handleShippingOptionsChange } from "../../../ppcp-button/resources/js/modules/Helper/ShippingHandler"; const config = wc.wcSettings.getSetting('ppcp-gateway_data'); window.ppcpFundingSource = config.fundingSource; let registeredContext = false; let paypalScriptPromise = null; const PayPalComponent = ({ onClick, onClose, onSubmit, onError, eventRegistration, emitResponse, activePaymentMethod, shippingData, isEditing, fundingSource, }) => { const {onPaymentSetup, onCheckoutFail, onCheckoutValidation} = eventRegistration; const {responseTypes} = emitResponse; const [paypalOrder, setPaypalOrder] = useState(null); const [gotoContinuationOnError, setGotoContinuationOnError] = useState(false); const [paypalScriptLoaded, setPaypalScriptLoaded] = useState(false); if (!paypalScriptLoaded) { if (!paypalScriptPromise) { // for editor, since canMakePayment was not called paypalScriptPromise = loadPaypalScriptPromise(config.scriptData) } paypalScriptPromise.then(() => setPaypalScriptLoaded(true)); } const methodId = fundingSource ? `${config.id}-${fundingSource}` : config.id; useEffect(() => { // fill the form if in continuation (for product or mini-cart buttons) if (!config.scriptData.continuation || !config.scriptData.continuation.order || window.ppcpContinuationFilled) { 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 window.ppcpContinuationFilled = true; }, []) const createOrder = async (data, actions) => { try { const res = await fetch(config.scriptData.ajax.create_order.endpoint, { method: 'POST', credentials: 'same-origin', body: JSON.stringify({ nonce: config.scriptData.ajax.create_order.nonce, bn_code: '', context: config.scriptData.context, payment_method: 'ppcp-gateway', funding_source: window.ppcpFundingSource ?? 'paypal', createaccount: false, payment_source: data.paymentSource }), }); 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('
')); } 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); let 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 (!shouldHandleShippingInPayPal()) { location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError(true); 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); let 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 (!shouldHandleShippingInPayPal()) { location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError(true); 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 = () => { 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 (shouldHandleShippingInPayPal()) { location.href = getCheckoutRedirectUrl(); } return true; }); return unsubscribe; }, [onCheckoutFail, onClose, activePaymentMethod]); if (config.scriptData.continuation) { return (
) } 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 (!paypalScriptLoaded) { return null; } const PayPalButton = paypal.Buttons.driver("react", { React, ReactDOM }); if(isPayPalSubscription(config.scriptData)) { return ( { shouldHandleShippingInPayPal() ? handleSubscriptionShippingOptionsChange(data, actions) : null; }} onShippingAddressChange={(data, actions) => { shouldHandleShippingInPayPal() ? handleSubscriptionShippingAddressChange(data, actions) : null; }} /> ); } return ( { shouldHandleShippingInPayPal() ? handleShippingOptionsChange(data, actions) : null; }} onShippingAddressChange={(data, actions) => { shouldHandleShippingInPayPal() ? handleShippingAddressChange(data, actions) : null; }} /> ); } const BlockEditorPayPalComponent = () => { const urlParams = { clientId: 'test', ...config.scriptData.url_params, dataNamespace: 'ppcp-blocks-editor-paypal-buttons', components: 'buttons', } return ( { return false; }} /> ) } 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 && config.enabled) { if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !config.scriptData.continuation) { let descriptionElement =
; if (config.placeOrderButtonDescription) { descriptionElement =

; } registerPaymentMethod({ name: config.id, label:
, content: descriptionElement, edit: descriptionElement, placeOrderButtonLabel: config.placeOrderButtonText, ariaLabel: config.title, canMakePayment: () => { return config.enabled; }, supports: { features: features, }, }); } if (config.scriptData.continuation) { registerPaymentMethod({ name: config.id, label:
, content: , edit: , ariaLabel: config.title, canMakePayment: () => { return true; }, supports: { features: [...features, 'ppcp_continuation'], }, }); } else if (!config.usePlaceOrder) { for (const fundingSource of ['paypal', ...config.enabledFundingSources]) { registerExpressPaymentMethod({ name: `${config.id}-${fundingSource}`, paymentMethodId: config.id, label:
, content: , edit: , ariaLabel: config.title, canMakePayment: async () => { if (!paypalScriptPromise) { paypalScriptPromise = loadPaypalScriptPromise(config.scriptData); paypalScriptPromise.then(() => { const messagesBootstrap = new BlockCheckoutMessagesBootstrap(config.scriptData); messagesBootstrap.init(); }); } await paypalScriptPromise; return paypal.Buttons({fundingSource}).isEligible(); }, supports: { features: features, }, }); } } }