Partial implementation of the button widget

This commit is contained in:
inpsyde-maticluznar 2024-11-27 08:02:06 +01:00
parent 5f83517509
commit 6225943c00
No known key found for this signature in database
GPG key ID: D005973F231309F6
33 changed files with 69 additions and 2463 deletions

View file

View file

@ -15,6 +15,7 @@
"dependencies": {
"@paypal/paypal-js": "^8.1.2",
"@woocommerce/settings": "^1.0.0",
"deepmerge": "^4.3.1",
"react-select": "^5.8.3"
}
}

View file

@ -2,6 +2,7 @@ import { __, sprintf } from '@wordpress/i18n';
import { SelectControl, RadioControl } from '@wordpress/components';
import { PayPalCheckboxGroup } from '../../ReusableComponents/Fields';
import { useState, useMemo, useEffect } from '@wordpress/element';
import Renderer from '../../../../../../ppcp-button/resources/js/modules/Renderer/Renderer';
import {
defaultLocationSettings,
paymentMethodOptions,
@ -10,7 +11,6 @@ import {
buttonLayoutOptions,
buttonLabelOptions,
} from '../../../data/settings/tab-styling-data';
import Renderer from '../../../utils/renderer';
const TabStyling = () => {
useEffect( () => {
@ -178,11 +178,8 @@ const TabStyling = () => {
</TabStylingSection>
) }
</div>
<div
id="ppcp-r-styling-preview"
className="ppcp-r-styling__preview"
>
<div className="ppcp-preview"></div>
<div className="ppcp-preview ppcp-r-button-preview ppcp-r-styling__preview">
<div id="ppcp-r-styling-preview"></div>
</div>
</div>
);
@ -229,6 +226,7 @@ const SectionIntro = () => {
};
const generatePreview = () => {
const render = () => {
const settings = {
button: {
wrapper: '#ppcp-r-styling-preview',
@ -242,15 +240,59 @@ const generatePreview = () => {
},
separate_buttons: {},
};
const wrapperSelector =
Object.values( settings.separate_buttons ).length > 0
? Object.values( settings.separate_buttons )[ 0 ].wrapper
: settings.button.wrapper;
const wrapper = document.querySelector( wrapperSelector );
if ( ! wrapper ) {
return;
}
wrapper.innerHTML = '';
const renderer = new Renderer(
null,
settings,
( data, actions ) => actions.reject(),
null
);
jQuery( document ).trigger( 'ppcp_paypal_render_preview', settings );
try {
renderer.render( {} );
jQuery( document ).trigger(
'ppcp_paypal_render_preview',
settings
);
} catch ( err ) {
console.error( err );
}
};
renderPreview( () => {
console.log( 'CALLBACK' );
}, render );
};
function renderPreview( settingsCallback, render ) {
let oldSettings = settingsCallback();
const form = jQuery( '#mainform' );
form.on( 'change', ':input', () => {
const newSettings = settingsCallback();
if ( JSON.stringify( oldSettings ) === JSON.stringify( newSettings ) ) {
return;
}
render( newSettings );
oldSettings = newSettings;
} );
jQuery( document ).on( 'ppcp_paypal_script_loaded', () => {
oldSettings = settingsCallback();
render( oldSettings );
} );
render( oldSettings );
}
export default TabStyling;

View file

@ -1,129 +0,0 @@
export const apmButtonsInit = ( config, selector = '.ppcp-button-apm' ) => {
let selectorInContainer = selector;
if ( window.ppcpApmButtons ) {
return;
}
if ( config && config.button ) {
// If it's separate gateways, modify wrapper to account for the individual buttons as individual APMs.
const wrapper = config.button.wrapper;
const isSeparateGateways =
jQuery( wrapper ).children( 'div[class^="item-"]' ).length > 0;
if ( isSeparateGateways ) {
selector += `, ${ wrapper } div[class^="item-"]`;
selectorInContainer += `, div[class^="item-"]`;
}
}
window.ppcpApmButtons = new ApmButtons( selector, selectorInContainer );
};
export class ApmButtons {
constructor( selector, selectorInContainer ) {
this.selector = selector;
this.selectorInContainer = selectorInContainer;
this.containers = [];
// Reloads button containers.
this.reloadContainers();
// Refresh button layout.
jQuery( window )
.resize( () => {
this.refresh();
} )
.resize();
jQuery( document ).on( 'ppcp-smart-buttons-init', () => {
this.refresh();
} );
jQuery( document ).on(
'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
( ev, data ) => {
this.refresh();
setTimeout( this.refresh.bind( this ), 200 );
}
);
// Observes for new buttons.
new MutationObserver(
this.observeElementsCallback.bind( this )
).observe( document.body, { childList: true, subtree: true } );
}
observeElementsCallback( mutationsList, observer ) {
const observeSelector =
this.selector +
', .widget_shopping_cart, .widget_shopping_cart_content';
let shouldReload = false;
for ( const mutation of mutationsList ) {
if ( mutation.type === 'childList' ) {
mutation.addedNodes.forEach( ( node ) => {
if ( node.matches && node.matches( observeSelector ) ) {
shouldReload = true;
}
} );
}
}
if ( shouldReload ) {
this.reloadContainers();
this.refresh();
}
}
reloadContainers() {
jQuery( this.selector ).each( ( index, el ) => {
const parent = jQuery( el ).parent();
if ( ! this.containers.some( ( $el ) => $el.is( parent ) ) ) {
this.containers.push( parent );
}
} );
}
refresh() {
for ( const container of this.containers ) {
const $container = jQuery( container );
// Check width and add classes
const width = $container.width();
$container.removeClass(
'ppcp-width-500 ppcp-width-300 ppcp-width-min'
);
if ( width >= 500 ) {
$container.addClass( 'ppcp-width-500' );
} else if ( width >= 300 ) {
$container.addClass( 'ppcp-width-300' );
} else {
$container.addClass( 'ppcp-width-min' );
}
// Check first apm button
const $firstElement = $container.children( ':visible' ).first();
// Assign margins to buttons
$container.find( this.selectorInContainer ).each( ( index, el ) => {
const $el = jQuery( el );
if ( $el.is( $firstElement ) ) {
$el.css( 'margin-top', `0px` );
return true;
}
const minMargin = 11; // Minimum margin.
const height = $el.height();
const margin = Math.max(
minMargin,
Math.round( height * 0.3 )
);
$el.css( 'margin-top', `${ margin }px` );
} );
}
}
}

View file

@ -1,54 +0,0 @@
import { disable, enable, isDisabled } from './ButtonDisabler';
import merge from 'deepmerge';
/**
* Common Bootstrap methods to avoid code repetition.
*/
export default class BootstrapHelper {
static handleButtonStatus( bs, options ) {
options = options || {};
options.wrapper = options.wrapper || bs.gateway.button.wrapper;
const wasDisabled = isDisabled( options.wrapper );
const shouldEnable = bs.shouldEnable();
// Handle enable / disable
if ( shouldEnable && wasDisabled ) {
bs.renderer.enableSmartButtons( options.wrapper );
enable( options.wrapper );
} else if ( ! shouldEnable && ! wasDisabled ) {
bs.renderer.disableSmartButtons( options.wrapper );
disable( options.wrapper, options.formSelector || null );
}
if ( wasDisabled !== ! shouldEnable ) {
jQuery( options.wrapper ).trigger( 'ppcp_buttons_enabled_changed', [
shouldEnable,
] );
}
}
static shouldEnable( bs, options ) {
options = options || {};
if ( typeof options.isDisabled === 'undefined' ) {
options.isDisabled = bs.gateway.button.is_disabled;
}
return bs.shouldRender() && options.isDisabled !== true;
}
static updateScriptData( bs, newData ) {
const newObj = merge( bs.gateway, newData );
const isChanged =
JSON.stringify( bs.gateway ) !== JSON.stringify( newObj );
bs.gateway = newObj;
if ( isChanged ) {
jQuery( document.body ).trigger( 'ppcp_script_data_changed', [
newObj,
] );
}
}
}

View file

@ -1,86 +0,0 @@
/**
* @param selectorOrElement
* @return {Element}
*/
const getElement = ( selectorOrElement ) => {
if ( typeof selectorOrElement === 'string' ) {
return document.querySelector( selectorOrElement );
}
return selectorOrElement;
};
const triggerEnabled = ( selectorOrElement, element ) => {
jQuery( document ).trigger( 'ppcp-enabled', {
handler: 'ButtonsDisabler.setEnabled',
action: 'enable',
selector: selectorOrElement,
element,
} );
};
const triggerDisabled = ( selectorOrElement, element ) => {
jQuery( document ).trigger( 'ppcp-disabled', {
handler: 'ButtonsDisabler.setEnabled',
action: 'disable',
selector: selectorOrElement,
element,
} );
};
export const setEnabled = ( selectorOrElement, enable, form = null ) => {
const element = getElement( selectorOrElement );
if ( ! element ) {
return;
}
if ( enable ) {
jQuery( element )
.removeClass( 'ppcp-disabled' )
.off( 'mouseup' )
.find( '> *' )
.css( 'pointer-events', '' );
triggerEnabled( selectorOrElement, element );
} else {
jQuery( element )
.addClass( 'ppcp-disabled' )
.on( 'mouseup', function ( event ) {
event.stopImmediatePropagation();
if ( form ) {
// Trigger form submit to show the error message
const $form = jQuery( form );
if (
$form
.find( '.single_add_to_cart_button' )
.hasClass( 'disabled' )
) {
$form.find( ':submit' ).trigger( 'click' );
}
}
} )
.find( '> *' )
.css( 'pointer-events', 'none' );
triggerDisabled( selectorOrElement, element );
}
};
export const isDisabled = ( selectorOrElement ) => {
const element = getElement( selectorOrElement );
if ( ! element ) {
return false;
}
return jQuery( element ).hasClass( 'ppcp-disabled' );
};
export const disable = ( selectorOrElement, form = null ) => {
setEnabled( selectorOrElement, false, form );
};
export const enable = ( selectorOrElement ) => {
setEnabled( selectorOrElement, true );
};

View file

@ -1,46 +0,0 @@
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
const REFRESH_BUTTON_EVENT = 'ppcp_refresh_payment_buttons';
/**
* Triggers a refresh of the payment buttons.
* This function dispatches a custom event that the button components listen for.
*
* Use this function on the front-end to update payment buttons after the checkout form was updated.
*/
export function refreshButtons() {
document.dispatchEvent( new Event( REFRESH_BUTTON_EVENT ) );
}
/**
* Sets up event listeners for various cart and checkout update events.
* When these events occur, it triggers a refresh of the payment buttons.
*
* @param {Function} refresh - Callback responsible to re-render the payment button.
*/
export function setupButtonEvents( refresh ) {
const miniCartInitDelay = 1000;
const debouncedRefresh = debounce( refresh, 50 );
// Listen for our custom refresh event.
document.addEventListener( REFRESH_BUTTON_EVENT, debouncedRefresh );
// Listen for cart and checkout update events.
// Note: we need jQuery here, because WooCommerce uses jQuery.trigger() to dispatch the events.
window
.jQuery( 'body' )
.on( 'updated_cart_totals', debouncedRefresh )
.on( 'updated_checkout', debouncedRefresh );
// Use setTimeout for fragment events to avoid unnecessary refresh on initial render.
setTimeout( () => {
document.body.addEventListener(
'wc_fragments_loaded',
debouncedRefresh
);
document.body.addEventListener(
'wc_fragments_refreshed',
debouncedRefresh
);
}, miniCartInitDelay );
}

View file

@ -1,77 +0,0 @@
class CartHelper {
constructor( cartItemKeys = [] ) {
this.cartItemKeys = cartItemKeys;
}
getEndpoint() {
let ajaxUrl = '/?wc-ajax=%%endpoint%%';
if (
typeof wc_cart_fragments_params !== 'undefined' &&
wc_cart_fragments_params.wc_ajax_url
) {
ajaxUrl = wc_cart_fragments_params.wc_ajax_url;
}
return ajaxUrl.toString().replace( '%%endpoint%%', 'remove_from_cart' );
}
addFromPurchaseUnits( purchaseUnits ) {
for ( const purchaseUnit of purchaseUnits || [] ) {
for ( const item of purchaseUnit.items || [] ) {
if ( ! item.cart_item_key ) {
continue;
}
this.cartItemKeys.push( item.cart_item_key );
}
}
return this;
}
removeFromCart() {
return new Promise( ( resolve, reject ) => {
if ( ! this.cartItemKeys || ! this.cartItemKeys.length ) {
resolve();
return;
}
const numRequests = this.cartItemKeys.length;
let numResponses = 0;
const tryToResolve = () => {
numResponses++;
if ( numResponses >= numRequests ) {
resolve();
}
};
for ( const cartItemKey of this.cartItemKeys ) {
const params = new URLSearchParams();
params.append( 'cart_item_key', cartItemKey );
if ( ! cartItemKey ) {
tryToResolve();
continue;
}
fetch( this.getEndpoint(), {
method: 'POST',
credentials: 'same-origin',
body: params,
} )
.then( function ( res ) {
return res.json();
} )
.then( () => {
tryToResolve();
} )
.catch( () => {
tryToResolve();
} );
}
} );
}
}
export default CartHelper;

View file

@ -1,55 +0,0 @@
import Spinner from './Spinner';
import FormValidator from './FormValidator';
import ErrorHandler from '../ErrorHandler';
const validateCheckoutForm = function ( config ) {
return new Promise( async ( resolve, reject ) => {
try {
const spinner = new Spinner();
const errorHandler = new ErrorHandler(
config.labels.error.generic,
document.querySelector( '.woocommerce-notices-wrapper' )
);
const formSelector =
config.context === 'checkout'
? 'form.checkout'
: 'form#order_review';
const formValidator = config.early_checkout_validation_enabled
? new FormValidator(
config.ajax.validate_checkout.endpoint,
config.ajax.validate_checkout.nonce
)
: null;
if ( ! formValidator ) {
resolve();
return;
}
formValidator
.validate( document.querySelector( formSelector ) )
.then( ( errors ) => {
if ( errors.length > 0 ) {
spinner.unblock();
errorHandler.clear();
errorHandler.messages( errors );
// fire WC event for other plugins
jQuery( document.body ).trigger( 'checkout_error', [
errorHandler.currentHtml(),
] );
reject();
} else {
resolve();
}
} );
} catch ( error ) {
console.error( error );
reject();
}
} );
};
export default validateCheckoutForm;

View file

@ -1,48 +0,0 @@
export const PaymentMethods = {
PAYPAL: 'ppcp-gateway',
CARDS: 'ppcp-credit-card-gateway',
OXXO: 'ppcp-oxxo-gateway',
CARD_BUTTON: 'ppcp-card-button-gateway',
GOOGLEPAY: 'ppcp-googlepay',
APPLEPAY: 'ppcp-applepay',
};
/**
* List of valid context values that the button can have.
*
* The "context" describes the placement or page where a payment button might be displayed.
*
* @type {Object}
*/
export const PaymentContext = {
Cart: 'cart', // Classic cart.
Checkout: 'checkout', // Classic checkout.
BlockCart: 'cart-block', // Block cart.
BlockCheckout: 'checkout-block', // Block checkout.
Product: 'product', // Single product page.
MiniCart: 'mini-cart', // Mini cart available on all pages except checkout & cart.
PayNow: 'pay-now', // Pay for order, via admin generated link.
Preview: 'preview', // Layout preview on settings page.
// Contexts that use blocks to render payment methods.
Blocks: [ 'cart-block', 'checkout-block' ],
// Contexts that display "classic" payment gateways.
Gateways: [ 'checkout', 'pay-now' ],
};
export const ORDER_BUTTON_SELECTOR = '#place_order';
export const getCurrentPaymentMethod = () => {
const el = document.querySelector( 'input[name="payment_method"]:checked' );
if ( ! el ) {
return null;
}
return el.value;
};
export const isSavedCardSelected = () => {
const savedCardList = document.querySelector( '#saved-credit-card' );
return savedCardList && savedCardList.value !== '';
};

View file

@ -1,31 +0,0 @@
import merge from 'deepmerge';
import { v4 as uuidv4 } from 'uuid';
import { keysToCamelCase } from './Utils';
const processAxoConfig = ( config ) => {
const scriptOptions = {};
const sdkClientToken = config?.axo?.sdk_client_token;
const uuid = uuidv4().replace( /-/g, '' );
if ( sdkClientToken && config?.user?.is_logged !== true ) {
scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken;
scriptOptions[ 'data-client-metadata-id' ] = uuid;
}
return scriptOptions;
};
const processUserIdToken = ( config ) => {
const userIdToken = config?.save_payment_methods?.id_token;
return userIdToken && config?.user?.is_logged === true
? { 'data-user-id-token': userIdToken }
: {};
};
export const processConfig = ( config ) => {
let scriptOptions = keysToCamelCase( config.url_params );
if ( config.script_attributes ) {
scriptOptions = merge( scriptOptions, config.script_attributes );
}
const axoOptions = processAxoConfig( config );
const userIdTokenOptions = processUserIdToken( config );
return merge.all( [ scriptOptions, axoOptions, userIdTokenOptions ] );
};

View file

@ -1,21 +0,0 @@
const dccInputFactory = ( original ) => {
const styles = window.getComputedStyle( original );
const newElement = document.createElement( 'span' );
newElement.setAttribute( 'id', original.id );
newElement.setAttribute( 'class', original.className );
Object.values( styles ).forEach( ( prop ) => {
if (
! styles[ prop ] ||
! isNaN( prop ) ||
prop === 'background-image'
) {
return;
}
newElement.style.setProperty( prop, '' + styles[ prop ] );
} );
return newElement;
};
export default dccInputFactory;

View file

@ -1,52 +0,0 @@
/**
* Common Form utility methods
*/
export default class FormHelper {
static getPrefixedFields( formElement, prefix ) {
const formData = new FormData( formElement );
const fields = {};
for ( const [ name, value ] of formData.entries() ) {
if ( ! prefix || name.startsWith( prefix ) ) {
fields[ name ] = value;
}
}
return fields;
}
static getFilteredFields( formElement, exactFilters, prefixFilters ) {
const formData = new FormData( formElement );
const fields = {};
const counters = {};
for ( let [ name, value ] of formData.entries() ) {
// Handle array format
if ( name.indexOf( '[]' ) !== -1 ) {
const k = name;
counters[ k ] = counters[ k ] || 0;
name = name.replace( '[]', `[${ counters[ k ] }]` );
counters[ k ]++;
}
if ( ! name ) {
continue;
}
if ( exactFilters && exactFilters.indexOf( name ) !== -1 ) {
continue;
}
if (
prefixFilters &&
prefixFilters.some( ( prefixFilter ) =>
name.startsWith( prefixFilter )
)
) {
continue;
}
fields[ name ] = value;
}
return fields;
}
}

View file

@ -1,28 +0,0 @@
export default class FormSaver {
constructor( url, nonce ) {
this.url = url;
this.nonce = nonce;
}
async save( form ) {
const formData = new FormData( form );
const res = await fetch( this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: this.nonce,
form_encoded: new URLSearchParams( formData ).toString(),
} ),
} );
const data = await res.json();
if ( ! data.success ) {
throw Error( data.data.message );
}
}
}

View file

@ -1,37 +0,0 @@
export default class FormValidator {
constructor( url, nonce ) {
this.url = url;
this.nonce = nonce;
}
async validate( form ) {
const formData = new FormData( form );
const res = await fetch( this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: this.nonce,
form_encoded: new URLSearchParams( formData ).toString(),
} ),
} );
const data = await res.json();
if ( ! data.success ) {
if ( data.data.refresh ) {
jQuery( document.body ).trigger( 'update_checkout' );
}
if ( data.data.errors ) {
return data.data.errors;
}
throw Error( data.data.message );
}
return [];
}
}

View file

@ -1,92 +0,0 @@
/**
* @param selectorOrElement
* @return {Element}
*/
const getElement = ( selectorOrElement ) => {
if ( typeof selectorOrElement === 'string' ) {
return document.querySelector( selectorOrElement );
}
return selectorOrElement;
};
const triggerHidden = ( handler, selectorOrElement, element ) => {
jQuery( document ).trigger( 'ppcp-hidden', {
handler,
action: 'hide',
selector: selectorOrElement,
element,
} );
};
const triggerShown = ( handler, selectorOrElement, element ) => {
jQuery( document ).trigger( 'ppcp-shown', {
handler,
action: 'show',
selector: selectorOrElement,
element,
} );
};
export const isVisible = ( element ) => {
return !! (
element.offsetWidth ||
element.offsetHeight ||
element.getClientRects().length
);
};
export const setVisible = ( selectorOrElement, show, important = false ) => {
const element = getElement( selectorOrElement );
if ( ! element ) {
return;
}
const currentValue = element.style.getPropertyValue( 'display' );
if ( ! show ) {
if ( currentValue === 'none' ) {
return;
}
element.style.setProperty(
'display',
'none',
important ? 'important' : ''
);
triggerHidden( 'Hiding.setVisible', selectorOrElement, element );
} else {
if ( currentValue === 'none' ) {
element.style.removeProperty( 'display' );
triggerShown( 'Hiding.setVisible', selectorOrElement, element );
}
// still not visible (if something else added display: none in CSS)
if ( ! isVisible( element ) ) {
element.style.setProperty( 'display', 'block' );
triggerShown( 'Hiding.setVisible', selectorOrElement, element );
}
}
};
export const setVisibleByClass = ( selectorOrElement, show, hiddenClass ) => {
const element = getElement( selectorOrElement );
if ( ! element ) {
return;
}
if ( show ) {
element.classList.remove( hiddenClass );
triggerShown( 'Hiding.setVisibleByClass', selectorOrElement, element );
} else {
element.classList.add( hiddenClass );
triggerHidden( 'Hiding.setVisibleByClass', selectorOrElement, element );
}
};
export const hide = ( selectorOrElement, important = false ) => {
setVisible( selectorOrElement, false, important );
};
export const show = ( selectorOrElement ) => {
setVisible( selectorOrElement, true );
};

View file

@ -1,179 +0,0 @@
/* global localStorage */
function checkLocalStorageAvailability() {
try {
const testKey = '__ppcp_test__';
localStorage.setItem( testKey, 'test' );
localStorage.removeItem( testKey );
return true;
} catch ( e ) {
return false;
}
}
function sanitizeKey( name ) {
return name
.toLowerCase()
.trim()
.replace( /[^a-z0-9_-]/g, '_' );
}
function deserializeEntry( serialized ) {
try {
const payload = JSON.parse( serialized );
return {
data: payload.data,
expires: payload.expires || 0,
};
} catch ( e ) {
return null;
}
}
function serializeEntry( data, timeToLive ) {
const payload = {
data,
expires: calculateExpiration( timeToLive ),
};
return JSON.stringify( payload );
}
function calculateExpiration( timeToLive ) {
return timeToLive ? Date.now() + timeToLive * 1000 : 0;
}
/**
* A reusable class for handling data storage in the browser's local storage,
* with optional expiration.
*
* Can be extended for module specific logic.
*
* @see GooglePaySession
*/
export class LocalStorage {
/**
* @type {string}
*/
#group = '';
/**
* @type {null|boolean}
*/
#canUseLocalStorage = null;
/**
* @param {string} group - Group name for all storage keys managed by this instance.
*/
constructor( group ) {
this.#group = sanitizeKey( group ) + ':';
this.#removeExpired();
}
/**
* Removes all items in the current group that have reached the expiry date.
*/
#removeExpired() {
if ( ! this.canUseLocalStorage ) {
return;
}
Object.keys( localStorage ).forEach( ( key ) => {
if ( ! key.startsWith( this.#group ) ) {
return;
}
const entry = deserializeEntry( localStorage.getItem( key ) );
if ( entry && entry.expires > 0 && entry.expires < Date.now() ) {
localStorage.removeItem( key );
}
} );
}
/**
* Sanitizes the given entry name and adds the group prefix.
*
* @throws {Error} If the name is empty after sanitization.
* @param {string} name - Entry name.
* @return {string} Prefixed and sanitized entry name.
*/
#entryKey( name ) {
const sanitizedName = sanitizeKey( name );
if ( sanitizedName.length === 0 ) {
throw new Error( 'Name cannot be empty after sanitization' );
}
return `${ this.#group }${ sanitizedName }`;
}
/**
* Indicates, whether localStorage is available.
*
* @return {boolean} True means the localStorage API is available.
*/
get canUseLocalStorage() {
if ( null === this.#canUseLocalStorage ) {
this.#canUseLocalStorage = checkLocalStorageAvailability();
}
return this.#canUseLocalStorage;
}
/**
* Stores data in the browser's local storage, with an optional timeout.
*
* @param {string} name - Name of the item in the storage.
* @param {any} data - The data to store.
* @param {number} [timeToLive=0] - Lifespan in seconds. 0 means the data won't expire.
* @throws {Error} If local storage is not available.
*/
set( name, data, timeToLive = 0 ) {
if ( ! this.canUseLocalStorage ) {
throw new Error( 'Local storage is not available' );
}
const entry = serializeEntry( data, timeToLive );
const entryKey = this.#entryKey( name );
localStorage.setItem( entryKey, entry );
}
/**
* Retrieves previously stored data from the browser's local storage.
*
* @param {string} name - Name of the stored item.
* @return {any|null} The stored data, or null when no valid entry is found or it has expired.
* @throws {Error} If local storage is not available.
*/
get( name ) {
if ( ! this.canUseLocalStorage ) {
throw new Error( 'Local storage is not available' );
}
const itemKey = this.#entryKey( name );
const entry = deserializeEntry( localStorage.getItem( itemKey ) );
if ( ! entry ) {
return null;
}
return entry.data;
}
/**
* Removes the specified entry from the browser's local storage.
*
* @param {string} name - Name of the stored item.
* @throws {Error} If local storage is not available.
*/
clear( name ) {
if ( ! this.canUseLocalStorage ) {
throw new Error( 'Local storage is not available' );
}
const itemKey = this.#entryKey( name );
localStorage.removeItem( itemKey );
}
}

View file

@ -1,123 +0,0 @@
import { refreshButtons } from './ButtonRefreshHelper';
const DEFAULT_TRIGGER_ELEMENT_SELECTOR = '.woocommerce-checkout-payment';
/**
* The MultistepCheckoutHelper class ensures the initialization of payment buttons
* on websites using a multistep checkout plugin. These plugins usually hide the
* payment button on page load up and reveal it later using JS. During the
* invisibility period of wrappers, some payment buttons fail to initialize,
* so we wait for the payment element to be visible.
*
* @property {HTMLElement} form - Checkout form element.
* @property {HTMLElement} triggerElement - Element, which visibility we need to detect.
* @property {boolean} isVisible - Whether the triggerElement is visible.
*/
class MultistepCheckoutHelper {
/**
* Selector that defines the HTML element we are waiting to become visible.
* @type {string}
*/
#triggerElementSelector;
/**
* Interval (in milliseconds) in which the visibility of the trigger element is checked.
* @type {number}
*/
#intervalTime = 150;
/**
* The interval ID returned by the setInterval() method.
* @type {number|false}
*/
#intervalId;
/**
* Selector passed to the constructor that identifies the checkout form
* @type {string}
*/
#formSelector;
/**
* @param {string} formSelector - Selector of the checkout form
* @param {string} triggerElementSelector - Optional. Selector of the dependant element.
*/
constructor( formSelector, triggerElementSelector = '' ) {
this.#formSelector = formSelector;
this.#triggerElementSelector =
triggerElementSelector || DEFAULT_TRIGGER_ELEMENT_SELECTOR;
this.#intervalId = false;
/*
Start the visibility checker after a brief delay. This allows eventual multistep plugins to
dynamically prepare the checkout page, so we can decide whether this helper is needed.
*/
setTimeout( () => {
if ( this.form && ! this.isVisible ) {
this.start();
}
}, 250 );
}
/**
* The checkout form element.
* @return {Element|null} - Form element or null.
*/
get form() {
return document.querySelector( this.#formSelector );
}
/**
* The element which must be visible before payment buttons should be initialized.
* @return {Element|null} - Trigger element or null.
*/
get triggerElement() {
return this.form?.querySelector( this.#triggerElementSelector );
}
/**
* Checks the visibility of the payment button wrapper.
* @return {boolean} - returns boolean value on the basis of visibility of element.
*/
get isVisible() {
const box = this.triggerElement?.getBoundingClientRect();
return !! ( box && box.width && box.height );
}
/**
* Starts the observation of the DOM, initiates monitoring the checkout form.
* To ensure multiple calls to start don't create multiple intervals, we first call stop.
*/
start() {
this.stop();
this.#intervalId = setInterval(
() => this.checkElement(),
this.#intervalTime
);
}
/**
* Stops the observation of the checkout form.
* Multiple calls to stop are safe as clearInterval doesn't throw if provided ID doesn't exist.
*/
stop() {
if ( this.#intervalId ) {
clearInterval( this.#intervalId );
this.#intervalId = false;
}
}
/**
* Checks if the trigger element is visible.
* If visible, it initialises the payment buttons and stops the observation.
*/
checkElement() {
if ( this.isVisible ) {
refreshButtons();
this.stop();
}
}
}
export default MultistepCheckoutHelper;

View file

@ -1,101 +0,0 @@
import { loadScript } from '@paypal/paypal-js';
import dataClientIdAttributeHandler from '../DataClientIdAttributeHandler';
import widgetBuilder from '../Renderer/WidgetBuilder';
import { processConfig } from './ConfigProcessor';
const loadedScripts = new Map();
const scriptPromises = new Map();
const handleDataClientIdAttribute = async ( scriptOptions, config ) => {
if (
config.data_client_id?.set_attribute &&
config.vault_v3_enabled !== true
) {
return new Promise( ( resolve, reject ) => {
dataClientIdAttributeHandler(
scriptOptions,
config.data_client_id,
( paypal ) => {
widgetBuilder.setPaypal( paypal );
resolve( paypal );
},
reject
);
} );
}
return null;
};
export const loadPayPalScript = async ( namespace, config ) => {
if ( ! namespace ) {
throw new Error( 'Namespace is required' );
}
if ( loadedScripts.has( namespace ) ) {
console.log( `Script already loaded for namespace: ${ namespace }` );
return loadedScripts.get( namespace );
}
if ( scriptPromises.has( namespace ) ) {
console.log(
`Script loading in progress for namespace: ${ namespace }`
);
return scriptPromises.get( namespace );
}
const scriptOptions = {
...processConfig( config ),
'data-namespace': namespace,
};
const dataClientIdResult = await handleDataClientIdAttribute(
scriptOptions,
config
);
if ( dataClientIdResult ) {
return dataClientIdResult;
}
const scriptPromise = new Promise( ( resolve, reject ) => {
loadScript( scriptOptions )
.then( ( script ) => {
widgetBuilder.setPaypal( script );
loadedScripts.set( namespace, script );
console.log( `Script loaded for namespace: ${ namespace }` );
resolve( script );
} )
.catch( ( error ) => {
console.error(
`Failed to load script for namespace: ${ namespace }`,
error
);
reject( error );
} )
.finally( () => {
scriptPromises.delete( namespace );
} );
} );
scriptPromises.set( namespace, scriptPromise );
return scriptPromise;
};
export const loadAndRenderPayPalScript = async (
namespace,
options,
renderFunction,
renderTarget
) => {
if ( ! namespace ) {
throw new Error( 'Namespace is required' );
}
const scriptOptions = {
...options,
'data-namespace': namespace,
};
const script = await loadScript( scriptOptions );
widgetBuilder.setPaypal( script );
await renderFunction( script, renderTarget );
};

View file

@ -1,196 +0,0 @@
/**
* Name details.
*
* @typedef {Object} NameDetails
* @property {string} [given_name] - First name, e.g. "John".
* @property {string} [surname] - Last name, e.g. "Doe".
*/
/**
* Postal address details.
*
* @typedef {Object} AddressDetails
* @property {string} [country_code] - Country code (2-letter).
* @property {string} [address_line_1] - Address details, line 1 (street, house number).
* @property {string} [address_line_2] - Address details, line 2.
* @property {string} [admin_area_1] - State or region.
* @property {string} [admin_area_2] - State or region.
* @property {string} [postal_code] - Zip code.
*/
/**
* Phone details.
*
* @typedef {Object} PhoneDetails
* @property {string} [phone_type] - Type, usually 'HOME'
* @property {{national_number: string}} [phone_number] - Phone number details.
*/
/**
* Payer details.
*
* @typedef {Object} PayerDetails
* @property {string} [email_address] - Email address for billing communication.
* @property {PhoneDetails} [phone] - Phone number for billing communication.
* @property {NameDetails} [name] - Payer's name.
* @property {AddressDetails} [address] - Postal billing address.
*/
// Map checkout fields to PayerData object properties.
const FIELD_MAP = {
'#billing_email': [ 'email_address' ],
'#billing_last_name': [ 'name', 'surname' ],
'#billing_first_name': [ 'name', 'given_name' ],
'#billing_country': [ 'address', 'country_code' ],
'#billing_address_1': [ 'address', 'address_line_1' ],
'#billing_address_2': [ 'address', 'address_line_2' ],
'#billing_state': [ 'address', 'admin_area_1' ],
'#billing_city': [ 'address', 'admin_area_2' ],
'#billing_postcode': [ 'address', 'postal_code' ],
'#billing_phone': [ 'phone' ],
};
function normalizePayerDetails( details ) {
return {
email_address: details.email_address,
phone: details.phone,
name: {
surname: details.name?.surname,
given_name: details.name?.given_name,
},
address: {
country_code: details.address?.country_code,
address_line_1: details.address?.address_line_1,
address_line_2: details.address?.address_line_2,
admin_area_1: details.address?.admin_area_1,
admin_area_2: details.address?.admin_area_2,
postal_code: details.address?.postal_code,
},
};
}
function mergePayerDetails( firstPayer, secondPayer ) {
const mergeNestedObjects = ( target, source ) => {
for ( const [ key, value ] of Object.entries( source ) ) {
if ( null !== value && undefined !== value ) {
if ( 'object' === typeof value ) {
target[ key ] = mergeNestedObjects(
target[ key ] || {},
value
);
} else {
target[ key ] = value;
}
}
}
return target;
};
return mergeNestedObjects(
normalizePayerDetails( firstPayer ),
normalizePayerDetails( secondPayer )
);
}
function getCheckoutBillingDetails() {
const getElementValue = ( selector ) =>
document.querySelector( selector )?.value;
const setNestedValue = ( obj, path, value ) => {
let current = obj;
for ( let i = 0; i < path.length - 1; i++ ) {
current = current[ path[ i ] ] = current[ path[ i ] ] || {};
}
current[ path[ path.length - 1 ] ] = value;
};
const data = {};
Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
const value = getElementValue( selector );
if ( value ) {
setNestedValue( data, path, value );
}
} );
if ( data.phone && 'string' === typeof data.phone ) {
data.phone = {
phone_type: 'HOME',
phone_number: { national_number: data.phone },
};
}
return data;
}
function setCheckoutBillingDetails( payer ) {
const setValue = ( path, field, value ) => {
if ( null === value || undefined === value || ! field ) {
return;
}
if ( 'phone' === path[ 0 ] && 'object' === typeof value ) {
value = value.phone_number?.national_number;
}
field.value = value;
};
const getNestedValue = ( obj, path ) =>
path.reduce( ( current, key ) => current?.[ key ], obj );
Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
const value = getNestedValue( payer, path );
const element = document.querySelector( selector );
setValue( path, element, value );
} );
}
export function getWooCommerceCustomerDetails() {
// Populated on server-side with details about the current WooCommerce customer.
return window?.PayPalCommerceGateway?.payer;
}
export function getSessionBillingDetails() {
// Populated by JS via `setSessionBillingDetails()`
return window._PpcpPayerSessionDetails;
}
/**
* Stores customer details in the current JS context for use in the same request.
* Details that are set are not persisted during navigation.
*
* @param {unknown} details - New payer details
*/
export function setSessionBillingDetails( details ) {
if ( ! details || 'object' !== typeof details ) {
return;
}
window._PpcpPayerSessionDetails = normalizePayerDetails( details );
}
export function payerData() {
const payer = getWooCommerceCustomerDetails() ?? getSessionBillingDetails();
if ( ! payer ) {
return null;
}
const formData = getCheckoutBillingDetails();
if ( formData ) {
return mergePayerDetails( payer, formData );
}
return normalizePayerDetails( payer );
}
export function setPayerData( payerDetails, updateCheckoutForm = false ) {
setSessionBillingDetails( payerDetails );
if ( updateCheckoutForm ) {
setCheckoutBillingDetails( payerDetails );
}
}

View file

@ -1,117 +0,0 @@
/**
* Helper function used by PaymentButton instances.
*
* @file
*/
/**
* Collection of recognized event names for payment button events.
*
* @type {Object}
*/
export const ButtonEvents = Object.freeze( {
INVALIDATE: 'ppcp_invalidate_methods',
RENDER: 'ppcp_render_method',
REDRAW: 'ppcp_redraw_method',
} );
/**
*
* @param {string} defaultId - Default wrapper ID.
* @param {string} miniCartId - Wrapper inside the mini-cart.
* @param {string} smartButtonId - ID of the smart button wrapper.
* @param {string} blockId - Block wrapper ID (express checkout, block cart).
* @param {string} gatewayId - Gateway wrapper ID (classic checkout).
* @return {{MiniCart, Gateway, Block, SmartButton, Default}} List of all wrapper IDs, by context.
*/
export function combineWrapperIds(
defaultId = '',
miniCartId = '',
smartButtonId = '',
blockId = '',
gatewayId = ''
) {
const sanitize = ( id ) => id.replace( /^#/, '' );
return {
Default: sanitize( defaultId ),
SmartButton: sanitize( smartButtonId ),
Block: sanitize( blockId ),
Gateway: sanitize( gatewayId ),
MiniCart: sanitize( miniCartId ),
};
}
/**
* Returns full payment button styles by combining the global ppcpConfig with
* payment-method-specific styling provided via buttonConfig.
*
* @param {Object} ppcpConfig - Global plugin configuration.
* @param {Object} buttonConfig - Payment method specific configuration.
* @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
*/
export function combineStyles( ppcpConfig, buttonConfig ) {
return {
Default: {
...ppcpConfig.style,
...buttonConfig.style,
},
MiniCart: {
...ppcpConfig.mini_cart_style,
...buttonConfig.mini_cart_style,
},
};
}
/**
* Verifies if the given event name is a valid Payment Button event.
*
* @param {string} event - The event name to verify.
* @return {boolean} True, if the event name is valid.
*/
export function isValidButtonEvent( event ) {
const buttonEventValues = Object.values( ButtonEvents );
return buttonEventValues.includes( event );
}
/**
* Dispatches a payment button event.
*
* @param {Object} options - The options for dispatching the event.
* @param {string} options.event - Event to dispatch.
* @param {string} [options.paymentMethod] - Optional. Name of payment method, to target a specific button only.
* @throws {Error} Throws an error if the event is invalid.
*/
export function dispatchButtonEvent( { event, paymentMethod = '' } ) {
if ( ! isValidButtonEvent( event ) ) {
throw new Error( `Invalid event: ${ event }` );
}
const fullEventName = paymentMethod
? `${ event }-${ paymentMethod }`
: event;
document.body.dispatchEvent( new Event( fullEventName ) );
}
/**
* Adds an event listener for the provided button event.
*
* @param {Object} options - The options for the event listener.
* @param {string} options.event - Event to observe.
* @param {string} [options.paymentMethod] - The payment method name (optional).
* @param {Function} options.callback - The callback function to execute when the event is triggered.
* @throws {Error} Throws an error if the event is invalid.
*/
export function observeButtonEvent( { event, paymentMethod = '', callback } ) {
if ( ! isValidButtonEvent( event ) ) {
throw new Error( `Invalid event: ${ event }` );
}
const fullEventName = paymentMethod
? `${ event }-${ paymentMethod }`
: event;
document.body.addEventListener( fullEventName, callback );
}

View file

@ -1,128 +0,0 @@
import dataClientIdAttributeHandler from '../DataClientIdAttributeHandler';
import { loadScript } from '@paypal/paypal-js';
import widgetBuilder from '../Renderer/WidgetBuilder';
import merge from 'deepmerge';
import { keysToCamelCase } from './Utils';
import { getCurrentPaymentMethod } from './CheckoutMethodState';
import { v4 as uuidv4 } from 'uuid';
// This component may be used by multiple modules. This assures that options are shared between all instances.
const scriptOptionsMap = {};
const getNamespaceOptions = ( namespace ) => {
if ( ! scriptOptionsMap[ namespace ] ) {
scriptOptionsMap[ namespace ] = {
isLoading: false,
onLoadedCallbacks: [],
onErrorCallbacks: [],
};
}
return scriptOptionsMap[ namespace ];
};
export const loadPaypalScript = ( config, onLoaded, onError = null ) => {
const dataNamespace = config?.data_namespace || '';
const options = getNamespaceOptions( dataNamespace );
// If PayPal is already loaded for this namespace, call the onLoaded callback and return.
if ( typeof window.paypal !== 'undefined' && ! dataNamespace ) {
onLoaded();
return;
}
// Add the onLoaded callback to the onLoadedCallbacks stack.
options.onLoadedCallbacks.push( onLoaded );
if ( onError ) {
options.onErrorCallbacks.push( onError );
}
// Return if it's still loading.
if ( options.isLoading ) {
return;
}
options.isLoading = true;
const resetState = () => {
options.isLoading = false;
options.onLoadedCallbacks = [];
options.onErrorCallbacks = [];
};
// Callback to be called once the PayPal script is loaded.
const callback = ( paypal ) => {
widgetBuilder.setPaypal( paypal );
for ( const onLoadedCallback of options.onLoadedCallbacks ) {
onLoadedCallback();
}
resetState();
};
const errorCallback = ( err ) => {
for ( const onErrorCallback of options.onErrorCallbacks ) {
onErrorCallback( err );
}
resetState();
};
// Build the PayPal script options.
let scriptOptions = keysToCamelCase( config.url_params );
if ( config.script_attributes ) {
scriptOptions = merge( scriptOptions, config.script_attributes );
}
// Axo SDK options
const sdkClientToken = config?.axo?.sdk_client_token;
const uuid = uuidv4().replace( /-/g, '' );
if ( sdkClientToken && config?.user?.is_logged !== true ) {
scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken;
scriptOptions[ 'data-client-metadata-id' ] = uuid;
}
// Load PayPal script for special case with data-client-token
if (
config.data_client_id?.set_attribute &&
config.vault_v3_enabled !== '1'
) {
dataClientIdAttributeHandler(
scriptOptions,
config.data_client_id,
callback,
errorCallback
);
return;
}
// Adds data-user-id-token to script options.
const userIdToken = config?.save_payment_methods?.id_token;
if ( userIdToken && config?.user?.is_logged === true ) {
scriptOptions[ 'data-user-id-token' ] = userIdToken;
}
// Adds data-namespace to script options.
if ( dataNamespace ) {
scriptOptions.dataNamespace = dataNamespace;
}
// Load PayPal script
loadScript( scriptOptions ).then( callback ).catch( errorCallback );
};
export const loadPaypalScriptPromise = ( config ) => {
return new Promise( ( resolve, reject ) => {
loadPaypalScript( config, resolve, reject );
} );
};
export const loadPaypalJsScript = ( options, buttons, container ) => {
loadScript( options ).then( ( paypal ) => {
paypal.Buttons( buttons ).render( container );
} );
};
export const loadPaypalJsScriptPromise = ( options ) => {
return new Promise( ( resolve, reject ) => {
loadScript( options ).then( resolve ).catch( reject );
} );
};

View file

@ -1,151 +0,0 @@
import { paypalAddressToWc } from '../../../../../ppcp-blocks/resources/js/Helper/Address.js';
import { convertKeysToSnakeCase } from '../../../../../ppcp-blocks/resources/js/Helper/Helper.js';
/**
* Handles the shipping option change in PayPal.
*
* @param data
* @param actions
* @param config
* @return {Promise<void>}
*/
export const handleShippingOptionsChange = async ( data, actions, config ) => {
try {
const shippingOptionId = data.selectedShippingOption?.id;
if ( shippingOptionId ) {
await fetch(
config.ajax.update_customer_shipping.shipping_options.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WC-Store-API-Nonce':
config.ajax.update_customer_shipping.wp_rest_nonce,
},
body: JSON.stringify( {
rate_id: shippingOptionId,
} ),
}
)
.then( ( response ) => {
return response.json();
} )
.then( ( cardData ) => {
const shippingMethods =
document.querySelectorAll( '.shipping_method' );
shippingMethods.forEach( function ( method ) {
if ( method.value === shippingOptionId ) {
method.checked = true;
}
} );
} );
}
if ( ! config.data_client_id.has_subscriptions ) {
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();
}
};
/**
* Handles the shipping address change in PayPal.
*
* @param data
* @param actions
* @param config
* @return {Promise<void>}
*/
export const handleShippingAddressChange = async ( data, actions, config ) => {
try {
const address = paypalAddressToWc(
convertKeysToSnakeCase( data.shippingAddress )
);
// Retrieve current cart contents
await fetch(
config.ajax.update_customer_shipping.shipping_address.cart_endpoint
)
.then( ( response ) => {
return response.json();
} )
.then( ( cartData ) => {
// Update shipping address in the cart data
cartData.shipping_address.address_1 = address.address_1;
cartData.shipping_address.address_2 = address.address_2;
cartData.shipping_address.city = address.city;
cartData.shipping_address.state = address.state;
cartData.shipping_address.postcode = address.postcode;
cartData.shipping_address.country = address.country;
// Send update request
return fetch(
config.ajax.update_customer_shipping.shipping_address
.update_customer_endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WC-Store-API-Nonce':
config.ajax.update_customer_shipping
.wp_rest_nonce,
},
body: JSON.stringify( {
shipping_address: cartData.shipping_address,
} ),
}
)
.then( function ( res ) {
return res.json();
} )
.then( function ( customerData ) {
jQuery( '.cart_totals .shop_table' ).load(
location.href +
' ' +
'.cart_totals .shop_table' +
'>*',
''
);
} );
} );
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();
}
};

View file

@ -1,42 +0,0 @@
class SimulateCart {
constructor( endpoint, nonce ) {
this.endpoint = endpoint;
this.nonce = nonce;
}
/**
*
* @param onResolve
* @param {Product[]} products
* @return {Promise<unknown>}
*/
simulate( onResolve, products ) {
return new Promise( ( resolve, reject ) => {
fetch( this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: this.nonce,
products,
} ),
} )
.then( ( result ) => {
return result.json();
} )
.then( ( result ) => {
if ( ! result.success ) {
reject( result.data );
return;
}
const resolved = onResolve( result.data );
resolve( resolved );
} );
} );
}
}
export default SimulateCart;

View file

@ -1,25 +0,0 @@
class Spinner {
constructor( target = 'form.woocommerce-checkout' ) {
this.target = target;
}
setTarget( target ) {
this.target = target;
}
block() {
jQuery( this.target ).block( {
message: null,
overlayCSS: {
background: '#fff',
opacity: 0.6,
},
} );
}
unblock() {
jQuery( this.target ).unblock();
}
}
export default Spinner;

View file

@ -1,20 +0,0 @@
export const normalizeStyleForFundingSource = ( style, fundingSource ) => {
const commonProps = {};
[ 'shape', 'height' ].forEach( ( prop ) => {
if ( style[ prop ] ) {
commonProps[ prop ] = style[ prop ];
}
} );
switch ( fundingSource ) {
case 'paypal':
return style;
case 'paylater':
return {
color: style.color,
...commonProps,
};
default:
return commonProps;
}
};

View file

@ -1,28 +0,0 @@
export const isChangePaymentPage = () => {
const urlParams = new URLSearchParams( window.location.search );
return urlParams.has( 'change_payment_method' );
};
export const getPlanIdFromVariation = ( variation ) => {
let subscription_plan = '';
PayPalCommerceGateway.variable_paypal_subscription_variations.forEach(
( element ) => {
const obj = {};
variation.forEach( ( { name, value } ) => {
Object.assign( obj, {
[ name.replace( 'attribute_', '' ) ]: value,
} );
} );
if (
JSON.stringify( obj ) ===
JSON.stringify( element.attributes ) &&
element.subscription_plan !== ''
) {
subscription_plan = element.subscription_plan;
}
}
);
return subscription_plan;
};

View file

@ -1,45 +0,0 @@
import Product from '../Entity/Product';
class UpdateCart {
constructor( endpoint, nonce ) {
this.endpoint = endpoint;
this.nonce = nonce;
}
/**
*
* @param onResolve
* @param {Product[]} products
* @param {Object} options
* @return {Promise<unknown>}
*/
update( onResolve, products, options = {} ) {
return new Promise( ( resolve, reject ) => {
fetch( this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: this.nonce,
products,
...options,
} ),
} )
.then( ( result ) => {
return result.json();
} )
.then( ( result ) => {
if ( ! result.success ) {
reject( result.data );
return;
}
const resolved = onResolve( result.data );
resolve( resolved );
} );
} );
}
}
export default UpdateCart;

View file

@ -1,69 +0,0 @@
export const toCamelCase = ( str ) => {
return str.replace( /([-_]\w)/g, function ( match ) {
return match[ 1 ].toUpperCase();
} );
};
export const keysToCamelCase = ( obj ) => {
const output = {};
for ( const key in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, key ) ) {
output[ toCamelCase( key ) ] = obj[ key ];
}
}
return output;
};
export const strAddWord = ( str, word, separator = ',' ) => {
const arr = str.split( separator );
if ( ! arr.includes( word ) ) {
arr.push( word );
}
return arr.join( separator );
};
export const strRemoveWord = ( str, word, separator = ',' ) => {
const arr = str.split( separator );
const index = arr.indexOf( word );
if ( index !== -1 ) {
arr.splice( index, 1 );
}
return arr.join( separator );
};
export const throttle = ( func, limit ) => {
let inThrottle, lastArgs, lastContext;
function execute() {
inThrottle = true;
func.apply( this, arguments );
setTimeout( () => {
inThrottle = false;
if ( lastArgs ) {
const nextArgs = lastArgs;
const nextContext = lastContext;
lastArgs = lastContext = null;
execute.apply( nextContext, nextArgs );
}
}, limit );
}
return function () {
if ( ! inThrottle ) {
execute.apply( this, arguments );
} else {
lastArgs = arguments;
lastContext = this;
}
};
};
const Utils = {
toCamelCase,
keysToCamelCase,
strAddWord,
strRemoveWord,
throttle,
};
export default Utils;

View file

@ -1,264 +0,0 @@
import merge from 'deepmerge';
import { loadScript } from '@paypal/paypal-js';
import { keysToCamelCase } from './Helper/Utils';
import widgetBuilder from './widget-builder';
import { normalizeStyleForFundingSource } from './Helper/Style';
class Renderer {
constructor(
creditCardRenderer,
defaultSettings,
onSmartButtonClick,
onSmartButtonsInit
) {
this.defaultSettings = defaultSettings;
this.creditCardRenderer = creditCardRenderer;
this.onSmartButtonClick = onSmartButtonClick;
this.onSmartButtonsInit = onSmartButtonsInit;
this.buttonsOptions = {};
this.onButtonsInitListeners = {};
this.renderedSources = new Set();
this.reloadEventName = 'ppcp-reload-buttons';
}
render(
contextConfig,
settingsOverride = {},
contextConfigOverride = () => {}
) {
const settings = merge( this.defaultSettings, settingsOverride );
const enabledSeparateGateways = Object.fromEntries(
Object.entries( settings.separate_buttons ).filter(
( [ s, data ] ) => document.querySelector( data.wrapper )
)
);
const hasEnabledSeparateGateways =
Object.keys( enabledSeparateGateways ).length !== 0;
if ( ! hasEnabledSeparateGateways ) {
console.log( 'RENDER 1', settings );
this.renderButtons(
settings.button.wrapper,
settings.button.style,
contextConfig,
hasEnabledSeparateGateways
);
} else {
// render each button separately
for ( const fundingSource of paypal
.getFundingSources()
.filter( ( s ) => ! ( s in enabledSeparateGateways ) ) ) {
const style = normalizeStyleForFundingSource(
settings.button.style,
fundingSource
);
console.log( 'RENDER' );
this.renderButtons(
settings.button.wrapper,
style,
contextConfig,
hasEnabledSeparateGateways,
fundingSource
);
}
}
if ( this.creditCardRenderer ) {
this.creditCardRenderer.render(
settings.hosted_fields.wrapper,
contextConfigOverride
);
}
for ( const [ fundingSource, data ] of Object.entries(
enabledSeparateGateways
) ) {
this.renderButtons(
data.wrapper,
data.style,
contextConfig,
hasEnabledSeparateGateways,
fundingSource
);
}
}
renderButtons(
wrapper,
style,
contextConfig,
hasEnabledSeparateGateways,
fundingSource = null
) {
if (
! document.querySelector( wrapper ) ||
this.isAlreadyRendered(
wrapper,
fundingSource,
hasEnabledSeparateGateways
)
) {
// Try to render registered buttons again in case they were removed from the DOM by an external source.
widgetBuilder.renderButtons( [ wrapper, fundingSource ] );
return;
}
if ( fundingSource ) {
contextConfig.fundingSource = fundingSource;
}
let venmoButtonClicked = false;
const buttonsOptions = () => {
const options = {
style,
...contextConfig,
onClick: ( data, actions ) => {
if ( this.onSmartButtonClick ) {
this.onSmartButtonClick( data, actions );
}
venmoButtonClicked = false;
if ( data.fundingSource === 'venmo' ) {
venmoButtonClicked = true;
}
},
onInit: ( data, actions ) => {
if ( this.onSmartButtonsInit ) {
this.onSmartButtonsInit( data, actions );
}
this.handleOnButtonsInit( wrapper, data, actions );
},
};
return options;
};
jQuery( document )
.off( this.reloadEventName, wrapper )
.on(
this.reloadEventName,
wrapper,
( event, settingsOverride = {}, triggeredFundingSource ) => {
// Only accept events from the matching funding source
if (
fundingSource &&
triggeredFundingSource &&
triggeredFundingSource !== fundingSource
) {
return;
}
const settings = merge(
this.defaultSettings,
settingsOverride
);
let scriptOptions = keysToCamelCase( settings.url_params );
scriptOptions = merge(
scriptOptions,
settings.script_attributes
);
loadScript( scriptOptions ).then( ( paypal ) => {
widgetBuilder.setPaypal( paypal );
widgetBuilder.registerButtons(
[ wrapper, fundingSource ],
buttonsOptions()
);
widgetBuilder.renderAll();
} );
}
);
this.renderedSources.add( wrapper + ( fundingSource ?? '' ) );
if (
typeof paypal !== 'undefined' &&
typeof paypal.Buttons !== 'undefined'
) {
widgetBuilder.registerButtons(
[ wrapper, fundingSource ],
buttonsOptions()
);
widgetBuilder.renderButtons( [ wrapper, fundingSource ] );
}
}
isVenmoButtonClickedWhenVaultingIsEnabled = ( venmoButtonClicked ) => {
return venmoButtonClicked && this.defaultSettings.vaultingEnabled;
};
shouldEnableShippingCallback = () => {
const needShipping =
this.defaultSettings.needShipping ||
this.defaultSettings.context === 'product';
return (
this.defaultSettings.should_handle_shipping_in_paypal &&
needShipping
);
};
isAlreadyRendered( wrapper, fundingSource ) {
return this.renderedSources.has( wrapper + ( fundingSource ?? '' ) );
}
disableCreditCardFields() {
this.creditCardRenderer.disableFields();
}
enableCreditCardFields() {
this.creditCardRenderer.enableFields();
}
onButtonsInit( wrapper, handler, reset ) {
this.onButtonsInitListeners[ wrapper ] = reset
? []
: this.onButtonsInitListeners[ wrapper ] || [];
this.onButtonsInitListeners[ wrapper ].push( handler );
}
handleOnButtonsInit( wrapper, data, actions ) {
this.buttonsOptions[ wrapper ] = {
data,
actions,
};
if ( this.onButtonsInitListeners[ wrapper ] ) {
for ( const handler of this.onButtonsInitListeners[ wrapper ] ) {
if ( typeof handler === 'function' ) {
handler( {
wrapper,
...this.buttonsOptions[ wrapper ],
} );
}
}
}
}
disableSmartButtons( wrapper ) {
if ( ! this.buttonsOptions[ wrapper ] ) {
return;
}
try {
this.buttonsOptions[ wrapper ].actions.disable();
} catch ( err ) {
console.log( 'Failed to disable buttons: ' + err );
}
}
enableSmartButtons( wrapper ) {
if ( ! this.buttonsOptions[ wrapper ] ) {
return;
}
try {
this.buttonsOptions[ wrapper ].actions.enable();
} catch ( err ) {
console.log( 'Failed to enable buttons: ' + err );
}
}
}
export default Renderer;

View file

@ -1,179 +0,0 @@
/**
* Handles the registration and rendering of PayPal widgets: Buttons and Messages.
* To have several Buttons per wrapper, an array should be provided, ex: [wrapper, fundingSource].
*/
class WidgetBuilder {
constructor() {
this.paypal = null;
this.buttons = new Map();
this.messages = new Map();
this.renderEventName = 'ppcp-render';
document.ppcpWidgetBuilderStatus = () => {
console.log( {
buttons: this.buttons,
messages: this.messages,
} );
};
jQuery( document )
.off( this.renderEventName )
.on( this.renderEventName, () => {
this.renderAll();
} );
}
setPaypal( paypal ) {
this.paypal = paypal;
jQuery( document ).trigger( 'ppcp-paypal-loaded', paypal );
}
registerButtons( wrapper, options ) {
wrapper = this.sanitizeWrapper( wrapper );
this.buttons.set( this.toKey( wrapper ), {
wrapper,
options,
} );
}
renderButtons( wrapper ) {
wrapper = this.sanitizeWrapper( wrapper );
if ( ! this.buttons.has( this.toKey( wrapper ) ) ) {
return;
}
if ( this.hasRendered( wrapper ) ) {
return;
}
const entry = this.buttons.get( this.toKey( wrapper ) );
const btn = this.paypal.Buttons( entry.options );
if ( ! btn.isEligible() ) {
this.buttons.delete( this.toKey( wrapper ) );
return;
}
const target = this.buildWrapperTarget( wrapper );
if ( ! target ) {
return;
}
btn.render( target );
}
renderAllButtons() {
for ( const [ wrapper, entry ] of this.buttons ) {
this.renderButtons( wrapper );
}
}
registerMessages( wrapper, options ) {
this.messages.set( wrapper, {
wrapper,
options,
} );
}
renderMessages( wrapper ) {
if ( ! this.messages.has( wrapper ) ) {
return;
}
const entry = this.messages.get( wrapper );
if ( this.hasRendered( wrapper ) ) {
const element = document.querySelector( wrapper );
element.setAttribute( 'data-pp-amount', entry.options.amount );
return;
}
const btn = this.paypal.Messages( entry.options );
btn.render( entry.wrapper );
// watchdog to try to handle some strange cases where the wrapper may not be present
setTimeout( () => {
if ( ! this.hasRendered( wrapper ) ) {
btn.render( entry.wrapper );
}
}, 100 );
}
renderAllMessages() {
for ( const [ wrapper, entry ] of this.messages ) {
this.renderMessages( wrapper );
}
}
renderAll() {
this.renderAllButtons();
this.renderAllMessages();
}
hasRendered( wrapper ) {
let selector = wrapper;
if ( Array.isArray( wrapper ) ) {
selector = wrapper[ 0 ];
for ( const item of wrapper.slice( 1 ) ) {
selector += ' .item-' + item;
}
}
const element = document.querySelector( selector );
return element && element.hasChildNodes();
}
sanitizeWrapper( wrapper ) {
if ( Array.isArray( wrapper ) ) {
wrapper = wrapper.filter( ( item ) => !! item );
if ( wrapper.length === 1 ) {
wrapper = wrapper[ 0 ];
}
}
return wrapper;
}
buildWrapperTarget( wrapper ) {
let target = wrapper;
if ( Array.isArray( wrapper ) ) {
const $wrapper = jQuery( wrapper[ 0 ] );
if ( ! $wrapper.length ) {
return;
}
const itemClass = 'item-' + wrapper[ 1 ];
// Check if the parent element exists and it doesn't already have the div with the class
let $item = $wrapper.find( '.' + itemClass );
if ( ! $item.length ) {
$item = jQuery( `<div class="${ itemClass }"></div>` );
$wrapper.append( $item );
}
target = $item.get( 0 );
}
if ( ! jQuery( target ).length ) {
return null;
}
return target;
}
toKey( wrapper ) {
if ( Array.isArray( wrapper ) ) {
return JSON.stringify( wrapper );
}
return wrapper;
}
}
export default window.widgetBuilder;

View file

@ -14,19 +14,5 @@ module.exports = {
),
style: path.resolve( process.cwd(), 'resources/css', 'style.scss' ),
},
resolve: {
...defaultConfig.resolve,
...{
alias: {
...defaultConfig.resolve.alias,
...{
ppcpButton: path.resolve(
__dirname,
'../ppcp-button/resources/js'
),
},
},
},
},
},
};

View file

@ -8,7 +8,7 @@
}
}
.ppcp-preview {
.ppcp-preview:not(.ppcp-r-styling__preview) {
width: var(--box-width, 100%);
padding: 15px;
border: 1px solid lightgray;