Add subscription support for Block Cart and Block Checkout.

This commit is contained in:
Pedro Silva 2024-01-10 18:12:01 +00:00
parent 02722a3c97
commit f3b30902db
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
3 changed files with 151 additions and 5 deletions

View file

@ -63,7 +63,7 @@ export const paypalAddressToWc = (address) => {
* @returns {Object} * @returns {Object}
*/ */
export const paypalShippingToWc = (shipping) => { export const paypalShippingToWc = (shipping) => {
const [firstName, lastName] = splitFullName(shipping.name.full_name); const [firstName, lastName] = (shipping.name ? splitFullName(shipping.name.full_name) : ['','']);
return { return {
...paypalAddressToWc(shipping.address), ...paypalAddressToWc(shipping.address),
first_name: firstName, first_name: firstName,
@ -87,6 +87,22 @@ export const paypalPayerToWc = (payer) => {
} }
} }
/**
* @param {Object} subscriber
* @returns {Object}
*/
export const paypalSubscriberToWc = (subscriber) => {
const firstName = subscriber?.name?.given_name ?? '';
const lastName = subscriber?.name?.surname ?? '';
const address = subscriber.address ? paypalAddressToWc(subscriber.shipping_address.address) : {};
return {
...address,
first_name: firstName,
last_name: lastName,
email: subscriber.email_address,
}
}
/** /**
* @param {Object} order * @param {Object} order
* @returns {Object} * @returns {Object}
@ -130,6 +146,17 @@ export const paypalOrderToWcAddresses = (order) => {
return {billingAddress, shippingAddress}; return {billingAddress, shippingAddress};
} }
/**
*
* @param subscription
* @returns {{shippingAddress: Object, billingAddress: Object}}
*/
export const paypalSubscriptionToWcAddresses = (subscription) => {
const shippingAddress = paypalSubscriberToWc(subscription.subscriber);
let billingAddress = shippingAddress;
return {billingAddress, shippingAddress};
}
/** /**
* Merges two WC addresses. * Merges two WC addresses.
* The objects can contain either the WC form fields or billingAddress, shippingAddress objects. * The objects can contain either the WC form fields or billingAddress, shippingAddress objects.

View file

@ -0,0 +1,16 @@
/**
* @param {Object} scriptData
* @returns {Boolean}
*/
export const isPayPalSubscription = (scriptData) => {
return scriptData.data_client_id.has_subscriptions
&& scriptData.data_client_id.paypal_subscriptions_enabled;
}
/**
* @param {Object} scriptData
* @returns {Boolean}
*/
export const cartHasSubscriptionProducts = (scriptData) => {
return !! scriptData?.locations_with_subscription_product?.cart;
}

View file

@ -1,6 +1,15 @@
import {useEffect, useState} from '@wordpress/element'; import {useEffect, useState} from '@wordpress/element';
import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry';
import {mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address"; import {
mergeWcAddress,
paypalAddressToWc,
paypalOrderToWcAddresses,
paypalSubscriptionToWcAddresses
} from "./Helper/Address";
import {
cartHasSubscriptionProducts,
isPayPalSubscription
} from "./Helper/Subscription";
import { import {
loadPaypalScriptPromise loadPaypalScriptPromise
} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
@ -9,7 +18,6 @@ import {
} from '../../../ppcp-button/resources/js/modules/Helper/Style' } from '../../../ppcp-button/resources/js/modules/Helper/Style'
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
import BlockCheckoutMessagesBootstrap from "./Bootstrap/BlockCheckoutMessagesBootstrap"; import BlockCheckoutMessagesBootstrap from "./Bootstrap/BlockCheckoutMessagesBootstrap";
const config = wc.wcSettings.getSetting('ppcp-gateway_data'); const config = wc.wcSettings.getSetting('ppcp-gateway_data');
window.ppcpFundingSource = config.fundingSource; window.ppcpFundingSource = config.fundingSource;
@ -106,6 +114,78 @@ const PayPalComponent = ({
} }
}; };
const createSubscription = async (data, actions) => {
return actions.subscription.create({
'plan_id': config.scriptData.subscription_plan_id
});
};
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 (!config.finalReviewEnabled) {
// 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 (config.finalReviewEnabled) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError(true);
onSubmit();
}
} catch (err) {
console.error(err);
onError(err.message);
onClose();
throw err;
}
};
const getCheckoutRedirectUrl = () => { const getCheckoutRedirectUrl = () => {
const checkoutUrl = new URL(config.scriptData.redirect); const checkoutUrl = new URL(config.scriptData.redirect);
// sometimes some browsers may load some kind of cached version of the page, // sometimes some browsers may load some kind of cached version of the page,
@ -328,6 +408,21 @@ const PayPalComponent = ({
const PayPalButton = paypal.Buttons.driver("react", { React, ReactDOM }); const PayPalButton = paypal.Buttons.driver("react", { React, ReactDOM });
if(isPayPalSubscription(config.scriptData)) {
return (
<PayPalButton
fundingSource={fundingSource}
style={style}
onClick={handleClick}
onCancel={onClose}
onError={onClose}
createSubscription={createSubscription}
onApprove={handleApproveSubscription}
onShippingChange={handleShippingChange}
/>
);
}
return ( return (
<PayPalButton <PayPalButton
fundingSource={fundingSource} fundingSource={fundingSource}
@ -344,6 +439,10 @@ const PayPalComponent = ({
const features = ['products']; const features = ['products'];
if(cartHasSubscriptionProducts(config.scriptData)) {
features.push('subscriptions');
}
if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !config.scriptData.continuation) { if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !config.scriptData.continuation) {
let descriptionElement = <div dangerouslySetInnerHTML={{__html: config.description}}></div>; let descriptionElement = <div dangerouslySetInnerHTML={{__html: config.description}}></div>;
if (config.placeOrderButtonDescription) { if (config.placeOrderButtonDescription) {
@ -360,7 +459,9 @@ if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !config.scriptData.c
edit: descriptionElement, edit: descriptionElement,
placeOrderButtonLabel: config.placeOrderButtonText, placeOrderButtonLabel: config.placeOrderButtonText,
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: () => config.enabled, canMakePayment: () => {
return config.enabled;
},
supports: { supports: {
features: features, features: features,
}, },
@ -374,7 +475,9 @@ if (config.scriptData.continuation) {
content: <PayPalComponent isEditing={false}/>, content: <PayPalComponent isEditing={false}/>,
edit: <PayPalComponent isEditing={true}/>, edit: <PayPalComponent isEditing={true}/>,
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: () => true, canMakePayment: () => {
return true;
},
supports: { supports: {
features: [...features, 'ppcp_continuation'], features: [...features, 'ppcp_continuation'],
}, },