woocommerce-paypal-payments/modules/ppcp-sdk-v6/resources/js/endpointsAdapter.js

245 lines
8 KiB
JavaScript

/**
* Adapter for the existing (v5) WC AJAX order endpoints.
*
* All v6 knowledge of the v5 endpoint contract lives here:
* ppc-change-cart, ppc-create-order, ppc-approve-order, ppc-update-shipping.
* Keep the request/response shapes in sync with the endpoint contract tests.
*
* @package
*/
import SingleProductActionHandler from '@ppcp-button/ActionHandler/SingleProductActionHandler';
import { payerData } from '@ppcp-button/Helper/PayerData';
import { postJson } from './utils/api';
/**
* Navigation seam: window.location is not mockable under jsdom, so
* redirects go through this indirection to stay unit-testable.
*/
export const navigation = {
assign: ( url ) => window.location.assign( url ),
};
/**
* Collects products from the single product form for ppc-change-cart.
*
* Reuses the v5 product collection, which handles simple, variable,
* grouped and booking products plus extra third-party form fields.
*
* @return {Object[]} Products in the { id, quantity, variations, extra, booking } shape.
* @throws {Error} When the product form cannot be found.
*/
function getProductsFromForm() {
// Classic themes render form.cart, block themes
// form.wc-block-add-to-cart-with-options; locate via the field.
const idElement =
document.querySelector( 'form [name="add-to-cart"]' ) ||
document.querySelector( 'form [name="product_id"]' );
const form = idElement?.closest( 'form' );
if ( ! form ) {
throw new Error( 'Product form not found.' );
}
const handler = new SingleProductActionHandler( null, null, form, null );
return handler.getProducts().map( ( product ) => product.data() );
}
/**
* Creates a PayPal order via the existing WC AJAX endpoints.
*
* On product pages the viewed product is first added to the cart
* (ppc-change-cart), matching the v5 flow; the returned purchase units
* are passed to ppc-create-order, which derives the product-context
* return URL from them.
*
* @param {Object} config - The wc_ppcp_sdk_v6 config object.
* @param {string} context - The page context.
* @param {string} fundingSource - The funding source (paypal, venmo, paylater).
* @return {Promise<{orderId: string}>} The created PayPal order id.
*/
export async function createOrder( config, context, fundingSource ) {
let purchaseUnits = [];
if ( context === 'product' ) {
purchaseUnits = await postJson( config.ajax.change_cart, {
products: getProductsFromForm(),
} );
}
const body = {
context,
purchase_units: purchaseUnits,
payment_method: 'ppcp-gateway',
funding_source: fundingSource || 'paypal',
save_order_in_session: 1,
};
if ( context === 'checkout' ) {
// Mirrors the v5 CheckoutActionHandler: the serialized form lets
// the server run the early WC checkout validation before creating
// the order, so the buyer sees form errors before approving.
const form = document.querySelector( 'form.checkout' );
if ( form ) {
body.form_encoded = new URLSearchParams(
new FormData( form )
).toString();
body.createaccount =
!! form.querySelector( '#createaccount' )?.checked;
}
const payer = payerData();
if ( payer ) {
body.payer = payer;
}
}
const data = await postJson( config.ajax.create_order, body );
return { orderId: data.id };
}
/**
* Approves the order and continues the purchase.
*
* Mirrors the v5 flow (onApproveForContinue): should_create_wc_order is
* requested like in v5 (except for Venmo with vaulting enabled), and the
* server decides — with the Pay Now experience enabled it creates the WC
* order right away and responds with order_received_url, skipping the
* Order Review page. Otherwise the endpoint only stores the approved
* order in the WC session and the buyer continues on checkout, where the
* gateway processes the session order on Place Order (also the fallback
* when order creation is not possible, e.g. a One-Touch approval without
* a shipping option selected inside the popup). On classic checkout the
* WC checkout form is submitted after approval instead.
*
* @param {Object} config - The wc_ppcp_sdk_v6 config object.
* @param {string} context - The page context.
* @param {string} fundingSource - The funding source used for payment.
* @param {string} orderId - The PayPal order ID.
*/
export async function approveOrder( config, context, fundingSource, orderId ) {
const canCreateOrder =
! config.vaulting_enabled || fundingSource !== 'venmo';
let data;
try {
data = await postJson( config.ajax.approve_order, {
order_id: orderId,
funding_source: fundingSource,
should_create_wc_order: canCreateOrder,
} );
} catch ( error ) {
if ( ! canCreateOrder ) {
throw error;
}
// e.g. One-Touch approval without a shipping option; fall back
// to the classic continuation on checkout.
data = await postJson( config.ajax.approve_order, {
order_id: orderId,
funding_source: fundingSource,
should_create_wc_order: false,
} );
}
if ( data?.order_received_url ) {
navigation.assign( data.order_received_url );
return;
}
if ( context === 'checkout' && typeof jQuery !== 'undefined' ) {
const checkoutForm = jQuery( 'form.checkout' );
if ( checkoutForm.length ) {
// The approved order must be processed by the PayPal gateway,
// not whichever payment method radio happens to be checked.
const gatewayRadio = document.querySelector(
'#payment_method_ppcp-gateway'
);
if ( gatewayRadio && ! gatewayRadio.checked ) {
gatewayRadio.checked = true;
jQuery( gatewayRadio ).trigger( 'change' );
}
checkoutForm.trigger( 'submit' );
return;
}
}
// Continuation: the buyer completes the order on the checkout page.
navigation.assign( config.urls.checkout );
}
/**
* Fetches the full PayPal order (ppc-get-order).
*
* Used by the block express flow to read the buyer's PayPal address,
* which the v6 session onApprove does not provide.
*
* @param {Object} config - The wc_ppcp_sdk_v6 config object.
* @param {string} orderId - The PayPal order ID.
* @return {Promise<Object>} The PayPal order (Orders v2 shape).
*/
export async function getOrder( config, orderId ) {
return postJson( config.ajax.get_order, {
order_id: orderId,
} );
}
/**
* Approves the order and stores it in the WC session without creating the
* WC order or redirecting.
*
* The block checkout submit creates the WC order through the gateway using
* the paypal_order_id, so unlike the classic approveOrder this must not
* create the order itself or navigate away.
*
* @param {Object} config - The wc_ppcp_sdk_v6 config object.
* @param {string} fundingSource - The funding source used for payment.
* @param {string} orderId - The PayPal order ID.
* @return {Promise<void>} Resolves when the order has been approved.
*/
export async function approveOrderInSession( config, fundingSource, orderId ) {
await postJson( config.ajax.approve_order, {
order_id: orderId,
funding_source: fundingSource,
should_create_wc_order: false,
} );
}
/**
* Patches the PayPal order with totals recalculated from the WC cart.
*
* @param {Object} config - The wc_ppcp_sdk_v6 config object.
* @param {string} orderId - The PayPal order ID.
* @return {Promise<void>} Resolves when the order has been patched.
*/
export async function updateShipping( config, orderId ) {
await postJson( config.ajax.update_shipping, {
order_id: orderId,
} );
}
/**
* Fetches the current cart total from the WC Store API, for refreshing
* amount-sensitive eligibility (Pay Later thresholds) after cart changes.
*
* @param {Object} config - The wc_ppcp_sdk_v6 config object.
* @return {Promise<string>} The total as a decimal string, or '' on failure.
*/
export async function fetchCartTotal( config ) {
try {
const response = await fetch( config.ajax.wc_store_api.cart, {
credentials: 'same-origin',
} );
const cart = await response.json();
const minorUnit = cart?.totals?.currency_minor_unit ?? 2;
const totalPrice = parseInt( cart?.totals?.total_price, 10 );
if ( isNaN( totalPrice ) ) {
return '';
}
return ( totalPrice / Math.pow( 10, minorUnit ) ).toFixed( minorUnit );
} catch ( error ) {
return '';
}
}