mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-04 08:47:23 +08:00
Merge pull request #1956 from woocommerce/PCP-2287-subscription-support-on-block-cart-block-express-checkout
Subscription support on Block Cart & Block Express Checkout (2287)
This commit is contained in:
commit
ba1e42df66
4 changed files with 248 additions and 53 deletions
|
@ -63,7 +63,7 @@ export const paypalAddressToWc = (address) => {
|
|||
* @returns {Object}
|
||||
*/
|
||||
export const paypalShippingToWc = (shipping) => {
|
||||
const [firstName, lastName] = splitFullName(shipping.name.full_name);
|
||||
const [firstName, lastName] = (shipping.name ? splitFullName(shipping.name.full_name) : ['','']);
|
||||
return {
|
||||
...paypalAddressToWc(shipping.address),
|
||||
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
|
||||
* @returns {Object}
|
||||
|
@ -130,6 +146,17 @@ export const paypalOrderToWcAddresses = (order) => {
|
|||
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.
|
||||
* The objects can contain either the WC form fields or billingAddress, shippingAddress objects.
|
||||
|
|
16
modules/ppcp-blocks/resources/js/Helper/Subscription.js
Normal file
16
modules/ppcp-blocks/resources/js/Helper/Subscription.js
Normal 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;
|
||||
}
|
|
@ -1,6 +1,15 @@
|
|||
import {useEffect, useState} from '@wordpress/element';
|
||||
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 {
|
||||
loadPaypalScriptPromise
|
||||
} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
|
||||
|
@ -9,7 +18,6 @@ import {
|
|||
} from '../../../ppcp-button/resources/js/modules/Helper/Style'
|
||||
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
|
||||
import BlockCheckoutMessagesBootstrap from "./Bootstrap/BlockCheckoutMessagesBootstrap";
|
||||
|
||||
const config = wc.wcSettings.getSetting('ppcp-gateway_data');
|
||||
|
||||
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 checkoutUrl = new URL(config.scriptData.redirect);
|
||||
// sometimes some browsers may load some kind of cached version of the page,
|
||||
|
@ -206,6 +286,7 @@ const PayPalComponent = ({
|
|||
};
|
||||
|
||||
let handleShippingChange = null;
|
||||
let handleSubscriptionShippingChange = null;
|
||||
if (shippingData.needsShipping && !config.finalReviewEnabled) {
|
||||
handleShippingChange = async (data, actions) => {
|
||||
try {
|
||||
|
@ -242,6 +323,30 @@ const PayPalComponent = ({
|
|||
actions.reject();
|
||||
}
|
||||
};
|
||||
|
||||
handleSubscriptionShippingChange = async (data, actions) => {
|
||||
console.log('--- handleSubscriptionShippingChange', data, actions);
|
||||
|
||||
try {
|
||||
const shippingOptionId = data.selected_shipping_option?.id;
|
||||
if (shippingOptionId) {
|
||||
await shippingData.setSelectedRates(shippingOptionId);
|
||||
}
|
||||
|
||||
const address = paypalAddressToWc(data.shipping_address);
|
||||
|
||||
await wp.data.dispatch('wc/store/cart').updateCustomerData({
|
||||
shipping_address: address,
|
||||
});
|
||||
|
||||
await shippingData.setShippingAddress(address);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
actions.reject();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -328,6 +433,21 @@ const PayPalComponent = ({
|
|||
|
||||
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={handleSubscriptionShippingChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PayPalButton
|
||||
fundingSource={fundingSource}
|
||||
|
@ -343,66 +463,95 @@ const PayPalComponent = ({
|
|||
}
|
||||
|
||||
const features = ['products'];
|
||||
let block_enabled = true;
|
||||
|
||||
if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !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>;
|
||||
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;
|
||||
}
|
||||
|
||||
registerPaymentMethod({
|
||||
name: config.id,
|
||||
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
|
||||
content: descriptionElement,
|
||||
edit: descriptionElement,
|
||||
placeOrderButtonLabel: config.placeOrderButtonText,
|
||||
ariaLabel: config.title,
|
||||
canMakePayment: () => config.enabled,
|
||||
supports: {
|
||||
features: features,
|
||||
},
|
||||
});
|
||||
// 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 (config.scriptData.continuation) {
|
||||
registerPaymentMethod({
|
||||
name: config.id,
|
||||
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
|
||||
content: <PayPalComponent isEditing={false}/>,
|
||||
edit: <PayPalComponent isEditing={true}/>,
|
||||
ariaLabel: config.title,
|
||||
canMakePayment: () => 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: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
|
||||
content: <PayPalComponent isEditing={false} fundingSource={fundingSource}/>,
|
||||
edit: <PayPalComponent isEditing={true} fundingSource={fundingSource}/>,
|
||||
ariaLabel: config.title,
|
||||
canMakePayment: async () => {
|
||||
if (!paypalScriptPromise) {
|
||||
paypalScriptPromise = loadPaypalScriptPromise(config.scriptData);
|
||||
paypalScriptPromise.then(() => {
|
||||
const messagesBootstrap = new BlockCheckoutMessagesBootstrap(config.scriptData);
|
||||
messagesBootstrap.init();
|
||||
});
|
||||
}
|
||||
await paypalScriptPromise;
|
||||
if (block_enabled) {
|
||||
if ((config.addPlaceOrderMethod || config.usePlaceOrder) && !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>;
|
||||
}
|
||||
|
||||
return paypal.Buttons({fundingSource}).isEligible();
|
||||
registerPaymentMethod({
|
||||
name: config.id,
|
||||
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
|
||||
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: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
|
||||
content: <PayPalComponent isEditing={false}/>,
|
||||
edit: <PayPalComponent isEditing={true}/>,
|
||||
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: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
|
||||
content: <PayPalComponent isEditing={false} fundingSource={fundingSource}/>,
|
||||
edit: <PayPalComponent isEditing={true} fundingSource={fundingSource}/>,
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1170,6 +1170,9 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
|
|||
'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled,
|
||||
'early_checkout_validation_enabled' => $this->early_validation_enabled,
|
||||
'funding_sources_without_redirect' => $this->funding_sources_without_redirect,
|
||||
'user' => array(
|
||||
'is_logged' => is_user_logged_in(),
|
||||
),
|
||||
);
|
||||
|
||||
if ( 'pay-now' === $this->context() ) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue