mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 18:16:38 +08:00
Create sidebar for styling screen and initalize preview - no success
This commit is contained in:
parent
06e74e74f4
commit
5f83517509
44 changed files with 3070 additions and 90 deletions
|
@ -13,6 +13,7 @@
|
|||
"@wordpress/scripts": "^30.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paypal/paypal-js": "^8.1.2",
|
||||
"@woocommerce/settings": "^1.0.0",
|
||||
"react-select": "^5.8.3"
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ button.components-button, a.components-button {
|
|||
|
||||
&:not(:disabled) {
|
||||
background-color: $color-blueberry;
|
||||
color:$color-white;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,23 +21,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__checkbox-value {
|
||||
@include hide-input-field;
|
||||
&__checkbox {
|
||||
position: relative;
|
||||
|
||||
&:not(:checked) + .ppcp-r__checkbox-presentation img {
|
||||
display: none;
|
||||
input {
|
||||
margin: 0;
|
||||
border-color: $color-gray-600;
|
||||
|
||||
&:checked {
|
||||
background-color: $color-blueberry;
|
||||
border-color:$color-blueberry;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ .ppcp-r__checkbox-presentation {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
.components-checkbox-control__input-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
.components-base-control__field {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
&__content {
|
||||
margin-top: 60px;
|
||||
padding:0 24px 28px 24px;
|
||||
padding: 0 24px 28px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,36 @@
|
|||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.components-radio-control {
|
||||
.components-flex {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
label {
|
||||
@include font(14, 20, 400);
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
&__option {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
border-color: $color-gray-700;
|
||||
margin-right: 0;
|
||||
&:checked {
|
||||
border: 1px solid $color-gray-700;
|
||||
background-color: $color-white;
|
||||
|
||||
&::before {
|
||||
transform: translate(3px, 3px);
|
||||
border-width: 6px;
|
||||
border-color: $color-blueberry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-r-modal__field-row--save button.is-primary {
|
||||
|
|
|
@ -18,10 +18,6 @@
|
|||
padding-top: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
@include font(14, 20, 400);
|
||||
}
|
||||
|
||||
&__inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -37,6 +33,16 @@
|
|||
color: $color-blueberry;
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-r__checkbox {
|
||||
.components-flex {
|
||||
gap: 12px;
|
||||
}
|
||||
label{
|
||||
@include font(13, 20, 400);
|
||||
color:$color-blueberry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-r-feature-item {
|
||||
|
@ -108,18 +114,20 @@
|
|||
gap: 12px;
|
||||
}
|
||||
|
||||
&__status-toggle--toggled{
|
||||
.ppcp-r-connection-status__show-all-data{
|
||||
transform:rotate(180deg);
|
||||
&__status-toggle--toggled {
|
||||
.ppcp-r-connection-status__show-all-data {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
*{
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
@include font(14, 24, 600);
|
||||
color: $color-gray-800;
|
||||
|
@ -131,11 +139,13 @@
|
|||
@include font(14, 24, 400);
|
||||
color: $color-gray-800;
|
||||
}
|
||||
.ppcp-r-connection-status__status-toggle{
|
||||
|
||||
.ppcp-r-connection-status__status-toggle {
|
||||
line-height: 0;
|
||||
}
|
||||
&--first{
|
||||
&:hover{
|
||||
|
||||
&--first {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
@ -148,11 +158,13 @@
|
|||
}
|
||||
&__status-row {
|
||||
flex-wrap: wrap;
|
||||
strong{
|
||||
|
||||
strong {
|
||||
width: 100%;
|
||||
}
|
||||
span{
|
||||
word-break:break-all;
|
||||
|
||||
span {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
.ppcp-r-styling {
|
||||
display: flex;
|
||||
border: 1px solid $color-gray-200;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&__section:not(:last-child) {
|
||||
border-bottom: 1px solid black;
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid $color-gray-600;
|
||||
}
|
||||
|
||||
&__main-title {
|
||||
@include font(14, 20, 600);
|
||||
color: $color-gray-800;
|
||||
margin: 0 0 8px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__description {
|
||||
@include font(13, 20, 400);
|
||||
color: $color-gray-800;
|
||||
margin: 0 0 18px 0;
|
||||
}
|
||||
|
||||
&__settings {
|
||||
width: 422px;
|
||||
background-color: $color-white;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
width: calc(100% - 422px);
|
||||
background-color: #FAF8F5;
|
||||
}
|
||||
|
||||
&__section--rc{
|
||||
.ppcp-r-styling__title {
|
||||
@include font(13, 20, 600);
|
||||
color: $color-black;
|
||||
display: block;
|
||||
margin: 0 0 18px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__select {
|
||||
label {
|
||||
@include font(13, 16, 600);
|
||||
color: $color-black;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
select {
|
||||
@include font(13, 20, 400);
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-r__checkbox {
|
||||
.components-checkbox-control {
|
||||
&__label {
|
||||
@include font(13, 20, 400);
|
||||
color: $color-black;
|
||||
}
|
||||
}
|
||||
|
||||
.components-flex {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__payment-method-checkboxes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-r {
|
||||
&__horizontal-control {
|
||||
.components-flex {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
|
||||
.components-radio-control {
|
||||
&__option {
|
||||
gap: 12px;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
@include font(13, 20, 400);
|
||||
color: $color-black;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@
|
|||
@import './components/screens/overview/tab-overview';
|
||||
@import './components/screens/overview/tab-payment-methods';
|
||||
@import './components/screens/overview/tab-settings';
|
||||
@import './components/screens/overview/tab-styling';
|
||||
}
|
||||
|
||||
@import './components/reusable-components/payment-method-modal';
|
||||
|
|
|
@ -1,25 +1,38 @@
|
|||
import data from '../../utils/data';
|
||||
import { CheckboxControl } from '@wordpress/components';
|
||||
|
||||
export const PayPalCheckbox = ( props ) => {
|
||||
return (
|
||||
<div className="ppcp-r__checkbox">
|
||||
<input
|
||||
className="ppcp-r__checkbox-value"
|
||||
type="checkbox"
|
||||
checked={ props.currentValue.includes( props.value ) }
|
||||
name={ props.name }
|
||||
<CheckboxControl
|
||||
label={ props?.label ? props.label : '' }
|
||||
value={ props.value }
|
||||
onChange={ ( e ) =>
|
||||
props.handleCheckboxState( e.target.checked, props )
|
||||
checked={ props.currentValue.includes( props.value ) }
|
||||
onChange={ ( checked ) =>
|
||||
handleCheckboxState( checked, props )
|
||||
}
|
||||
/>
|
||||
<span className="ppcp-r__checkbox-presentation">
|
||||
{ data().getImage( 'icon-checkbox.svg' ) }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PayPalCheckboxGroup = ( props ) => {
|
||||
const renderCheckboxGroup = () => {
|
||||
return props.value.map( ( checkbox ) => {
|
||||
return (
|
||||
<PayPalCheckbox
|
||||
label={ checkbox.label }
|
||||
value={ checkbox.value }
|
||||
key={ checkbox.value }
|
||||
currentValue={ props.currentValue }
|
||||
changeCallback={ props.changeCallback }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
};
|
||||
|
||||
return <>{ renderCheckboxGroup() }</>;
|
||||
};
|
||||
|
||||
export const PayPalRdb = ( props ) => {
|
||||
return (
|
||||
<div className="ppcp-r__radio">
|
||||
|
@ -75,7 +88,6 @@ export const handleCheckboxState = ( checked, props ) => {
|
|||
let newValue = null;
|
||||
if ( checked ) {
|
||||
newValue = [ ...props.currentValue, props.value ];
|
||||
props.changeCallback( newValue );
|
||||
} else {
|
||||
newValue = props.currentValue.filter(
|
||||
( value ) => value !== props.value
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import data from '../../utils/data';
|
||||
import { PayPalCheckbox, PayPalRdb, handleCheckboxState } from './Fields';
|
||||
import { PayPalCheckbox, PayPalRdb } from './Fields';
|
||||
|
||||
const SelectBox = ( props ) => {
|
||||
let boxClassName = 'ppcp-r-select-box';
|
||||
|
@ -24,10 +24,7 @@ const SelectBox = ( props ) => {
|
|||
) }
|
||||
{ props.type === 'checkbox' && (
|
||||
<PayPalCheckbox
|
||||
{ ...{
|
||||
...props,
|
||||
handleCheckboxState,
|
||||
} }
|
||||
{ ...props }
|
||||
/>
|
||||
) }
|
||||
<div className="ppcp-r-select-box__content">
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import {
|
||||
PayPalRdb,
|
||||
PayPalRdbWithContent,
|
||||
} from '../../../ReusableComponents/Fields';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { RadioControl } from '@wordpress/components';
|
||||
|
||||
const THREED_SECURE_GROUP_NAME = 'threed-secure';
|
||||
const ModalAcdc = ( { setModalIsVisible } ) => {
|
||||
const [ threeDSecure, setThreeDSecure ] = useState( 'no-3d-secure' );
|
||||
const acdcOptions = [
|
||||
{
|
||||
label: __( 'No 3D Secure', 'woocommerce-paypal-payments' ),
|
||||
value: 'no-3d-secure',
|
||||
},
|
||||
{
|
||||
label: __( 'Only when required', 'woocommerce-paypal-payments' ),
|
||||
value: 'only-required-3d-secure',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Always require 3D Secure',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
value: 'always-3d-secure',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PaymentMethodModal
|
||||
|
@ -30,39 +43,10 @@ const ModalAcdc = ( { setModalIsVisible } ) => {
|
|||
) }
|
||||
</p>
|
||||
<div className="ppcp-r-modal__field-rows ppcp-r-modal__field-rows--acdc">
|
||||
<PayPalRdbWithContent
|
||||
id="no-3d-secure"
|
||||
name={ THREED_SECURE_GROUP_NAME }
|
||||
value="no-3d-secure"
|
||||
currentValue={ threeDSecure }
|
||||
handleRdbState={ setThreeDSecure }
|
||||
label={ __(
|
||||
'No 3D Secure',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<PayPalRdbWithContent
|
||||
id="only-required-3d-secure"
|
||||
name={ THREED_SECURE_GROUP_NAME }
|
||||
value="only-required-3d-secure"
|
||||
currentValue={ threeDSecure }
|
||||
handleRdbState={ setThreeDSecure }
|
||||
label={ __(
|
||||
'Only when required',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
|
||||
<PayPalRdbWithContent
|
||||
id="always-3d-secure"
|
||||
name={ THREED_SECURE_GROUP_NAME }
|
||||
value="always-3d-secure"
|
||||
currentValue={ threeDSecure }
|
||||
handleRdbState={ setThreeDSecure }
|
||||
label={ __(
|
||||
'Always require 3D Secure',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
<RadioControl
|
||||
onChange={ setThreeDSecure }
|
||||
selected={ threeDSecure }
|
||||
options={ acdcOptions }
|
||||
/>
|
||||
|
||||
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
|
||||
|
|
|
@ -2,7 +2,6 @@ import SettingsCard from '../../ReusableComponents/SettingsCard';
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
PayPalCheckbox,
|
||||
handleCheckboxState,
|
||||
} from '../../ReusableComponents/Fields';
|
||||
import { useState } from '@wordpress/element';
|
||||
import data from '../../../utils/data';
|
||||
|
@ -41,7 +40,7 @@ const TabOverview = () => {
|
|||
value={ todo.value }
|
||||
currentValue={ todos }
|
||||
changeCallback={ setTodos }
|
||||
description={ todo.description }
|
||||
label={ todo.description }
|
||||
changeTodos={ setTodosData }
|
||||
todosData={ todosData }
|
||||
/>
|
||||
|
@ -144,10 +143,8 @@ const TodoItem = ( props ) => {
|
|||
<PayPalCheckbox
|
||||
{ ...{
|
||||
...props,
|
||||
handleCheckboxState,
|
||||
} }
|
||||
/>{ ' ' }
|
||||
<p>{ props.description }</p>
|
||||
</div>
|
||||
<div
|
||||
className="ppcp-r-todo-item__close"
|
||||
|
|
|
@ -24,7 +24,6 @@ const TabSettings = () => {
|
|||
buttonLanguage: '',
|
||||
} );
|
||||
const updateFormValue = ( key, value ) => {
|
||||
console.log( key, value );
|
||||
setSettings( { ...settings, [ key ]: value } );
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,256 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { SelectControl, RadioControl } from '@wordpress/components';
|
||||
import { PayPalCheckboxGroup } from '../../ReusableComponents/Fields';
|
||||
import { useState, useMemo, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
defaultLocationSettings,
|
||||
paymentMethodOptions,
|
||||
colorOptions,
|
||||
shapeOptions,
|
||||
buttonLayoutOptions,
|
||||
buttonLabelOptions,
|
||||
} from '../../../data/settings/tab-styling-data';
|
||||
import Renderer from '../../../utils/renderer';
|
||||
|
||||
const TabStyling = () => {
|
||||
return <div>Styling tab</div>;
|
||||
useEffect( () => {
|
||||
generatePreview();
|
||||
}, [] );
|
||||
|
||||
const [ location, setLocation ] = useState( 'cart' );
|
||||
const [ locationSettings, setLocationSettings ] = useState( {
|
||||
...defaultLocationSettings,
|
||||
} );
|
||||
|
||||
const currentLocationSettings = useMemo( () => {
|
||||
return locationSettings[ location ];
|
||||
}, [ location, locationSettings ] );
|
||||
|
||||
const locationOptions = useMemo( () => {
|
||||
return Object.keys( locationSettings ).reduce(
|
||||
( locationOptionsData, key ) => {
|
||||
locationOptionsData.push( {
|
||||
value: locationSettings[ key ].value,
|
||||
label: locationSettings[ key ].label,
|
||||
} );
|
||||
|
||||
return locationOptionsData;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}, [] );
|
||||
|
||||
const updateButtonSettings = ( key, value ) => {
|
||||
const updatedLocationSettings = { ...currentLocationSettings };
|
||||
|
||||
updatedLocationSettings.settings = {
|
||||
...updatedLocationSettings.settings,
|
||||
...{ [ key ]: value },
|
||||
};
|
||||
|
||||
setLocationSettings( {
|
||||
...locationSettings,
|
||||
[ location ]: { ...updatedLocationSettings },
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-styling">
|
||||
<div className="ppcp-r-styling__settings">
|
||||
<SectionIntro />
|
||||
<TabStylingSection className="ppcp-r-styling__section--rc">
|
||||
<SelectControl
|
||||
className="ppcp-r-styling__select"
|
||||
value={ location }
|
||||
onChange={ ( newLocation ) =>
|
||||
setLocation( newLocation )
|
||||
}
|
||||
label={ __(
|
||||
'Locations',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
options={ locationOptions }
|
||||
/>
|
||||
</TabStylingSection>
|
||||
<TabStylingSection
|
||||
title={ __(
|
||||
'Payment Methods',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
className="ppcp-r-styling__section--rc"
|
||||
>
|
||||
<div className="ppcp-r-styling__payment-method-checkboxes">
|
||||
<PayPalCheckboxGroup
|
||||
value={ paymentMethodOptions }
|
||||
changeCallback={ ( newValue ) =>
|
||||
updateButtonSettings(
|
||||
'paymentMethods',
|
||||
newValue
|
||||
)
|
||||
}
|
||||
currentValue={
|
||||
currentLocationSettings.settings.paymentMethods
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabStylingSection>
|
||||
{ currentLocationSettings.settings?.buttonLayout && (
|
||||
<TabStylingSection
|
||||
className="ppcp-r-styling__section--rc"
|
||||
title={ __(
|
||||
'Button Layout',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
>
|
||||
<RadioControl
|
||||
className="ppcp-r__horizontal-control"
|
||||
onChange={ ( newValue ) =>
|
||||
updateButtonSettings( 'buttonLayout', newValue )
|
||||
}
|
||||
selected={
|
||||
currentLocationSettings.settings.buttonLayout
|
||||
}
|
||||
options={ buttonLayoutOptions }
|
||||
/>
|
||||
</TabStylingSection>
|
||||
) }
|
||||
<TabStylingSection
|
||||
title={ __( 'Shape', 'woocommerce-paypal-payments' ) }
|
||||
className="ppcp-r-styling__section--rc"
|
||||
>
|
||||
<RadioControl
|
||||
className="ppcp-r__horizontal-control"
|
||||
onChange={ ( newValue ) =>
|
||||
updateButtonSettings( 'shape', newValue )
|
||||
}
|
||||
selected={ currentLocationSettings.settings.shape }
|
||||
options={ shapeOptions }
|
||||
/>
|
||||
</TabStylingSection>
|
||||
<TabStylingSection>
|
||||
<SelectControl
|
||||
className="ppcp-r-styling__select"
|
||||
onChange={ ( newValue ) =>
|
||||
updateButtonSettings( 'buttonLabel', newValue )
|
||||
}
|
||||
selected={
|
||||
currentLocationSettings.settings.buttonLabel
|
||||
}
|
||||
label={ __(
|
||||
'Button Label',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
options={ buttonLabelOptions }
|
||||
/>
|
||||
</TabStylingSection>
|
||||
<TabStylingSection>
|
||||
<SelectControl
|
||||
className="ppcp-r-styling__select"
|
||||
label={ __(
|
||||
'Button Color',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
options={ colorOptions }
|
||||
/>
|
||||
</TabStylingSection>
|
||||
{ currentLocationSettings.settings?.tagLine && (
|
||||
<TabStylingSection
|
||||
title={ __( 'Tagline', 'woocommerce-paypal-payments' ) }
|
||||
className="ppcp-r-styling__section--rc"
|
||||
>
|
||||
<PayPalCheckboxGroup
|
||||
value={ [
|
||||
{
|
||||
value: 'enable-tagline',
|
||||
label: __(
|
||||
'Enable Tagline',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
},
|
||||
] }
|
||||
changeCallback={ ( newValue ) =>
|
||||
updateButtonSettings( 'tagLine', newValue )
|
||||
}
|
||||
currentValue={
|
||||
currentLocationSettings.settings.tagLine
|
||||
}
|
||||
/>
|
||||
</TabStylingSection>
|
||||
) }
|
||||
</div>
|
||||
<div
|
||||
id="ppcp-r-styling-preview"
|
||||
className="ppcp-r-styling__preview"
|
||||
>
|
||||
<div className="ppcp-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TabStylingSection = ( props ) => {
|
||||
let sectionTitleClassName = 'ppcp-r-styling__section';
|
||||
|
||||
if ( props?.className ) {
|
||||
sectionTitleClassName += ` ${ props.className }`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ sectionTitleClassName }>
|
||||
<span className="ppcp-r-styling__title">{ props.title }</span>
|
||||
{ props?.description && (
|
||||
<p
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: props.description,
|
||||
} }
|
||||
className="ppcp-r-styling__description"
|
||||
/>
|
||||
) }
|
||||
{ props.children }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SectionIntro = () => {
|
||||
const buttonStyleDescription = sprintf(
|
||||
// translators: %s: Link to Classic checkout page
|
||||
__(
|
||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a>. Checkout Buttons must be enabled to display the PayPal gateway on the Checkout page.'
|
||||
),
|
||||
'#'
|
||||
);
|
||||
return (
|
||||
<TabStylingSection
|
||||
className="ppcp-r-styling__section--rc ppcp-r-styling__section--empty"
|
||||
title={ __( 'Button Styling', 'wooocommerce-paypal-payments' ) }
|
||||
description={ buttonStyleDescription }
|
||||
></TabStylingSection>
|
||||
);
|
||||
};
|
||||
|
||||
const generatePreview = () => {
|
||||
const settings = {
|
||||
button: {
|
||||
wrapper: '#ppcp-r-styling-preview',
|
||||
style: {
|
||||
color: 'gold',
|
||||
shape: 'rect',
|
||||
label: 'paypal',
|
||||
tagline: false,
|
||||
layout: 'horizontal',
|
||||
},
|
||||
},
|
||||
separate_buttons: {},
|
||||
};
|
||||
const renderer = new Renderer(
|
||||
null,
|
||||
settings,
|
||||
( data, actions ) => actions.reject(),
|
||||
null
|
||||
);
|
||||
jQuery( document ).trigger( 'ppcp_paypal_render_preview', settings );
|
||||
|
||||
renderer.render( {} );
|
||||
};
|
||||
|
||||
export default TabStyling;
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const settings = {
|
||||
paymentMethods: [],
|
||||
buttonLayout: 'horizontal',
|
||||
shape: null,
|
||||
buttonLabel: 'paypal',
|
||||
buttonColor: 'gold',
|
||||
tagLine: [],
|
||||
};
|
||||
|
||||
const cartAndExpressCheckoutSettings = {
|
||||
paymentMethods: [],
|
||||
shape: null,
|
||||
buttonLabel: 'paypal',
|
||||
buttonColor: 'gold',
|
||||
};
|
||||
|
||||
export const defaultLocationSettings = {
|
||||
cart: {
|
||||
value: 'cart',
|
||||
label: __( 'Cart', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...cartAndExpressCheckoutSettings },
|
||||
},
|
||||
'classic-checkout': {
|
||||
value: 'classic-checkout',
|
||||
label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...settings },
|
||||
},
|
||||
'express-checkout': {
|
||||
value: 'express-checkout',
|
||||
label: __( 'Express Checkout', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...cartAndExpressCheckoutSettings },
|
||||
},
|
||||
'mini-cart': {
|
||||
value: 'mini-cart',
|
||||
label: __( 'Mini Cart', 'woocommerce-paypel-payements' ),
|
||||
settings: { ...settings },
|
||||
},
|
||||
'product-page': {
|
||||
value: 'product-page',
|
||||
label: __( 'Product Page', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...settings },
|
||||
},
|
||||
};
|
||||
|
||||
export const paymentMethodOptions = [
|
||||
{
|
||||
value: 'venmo',
|
||||
label: __( 'Venmo', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'pay-later',
|
||||
label: __( 'Pay Later', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'debit-or-credit-card',
|
||||
label: __( 'Debit or Credit Card', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'google-pay',
|
||||
label: __( 'Google Pay', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'apple-pay',
|
||||
label: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
];
|
||||
|
||||
export const buttonLabelOptions = [
|
||||
{
|
||||
value: 'paypal',
|
||||
label: __( 'PayPal', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'checkout',
|
||||
label: __( 'Checkout', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'paypal-buy-now',
|
||||
label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'pay-with-paypal',
|
||||
label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
];
|
||||
|
||||
export const colorOptions = [
|
||||
{
|
||||
value: 'gold',
|
||||
label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'blue',
|
||||
label: __( 'Blue', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'silver',
|
||||
label: __( 'Silver', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'black',
|
||||
label: __( 'Black', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'white',
|
||||
label: __( 'White', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
];
|
||||
|
||||
export const buttonLayoutOptions = [
|
||||
{
|
||||
label: __( 'Vertical', 'woocommerce-paypal-payments' ),
|
||||
value: 'vertical',
|
||||
},
|
||||
{
|
||||
label: __( 'Horizontal', 'woocommerce-paypal-payments' ),
|
||||
value: 'horizontal',
|
||||
},
|
||||
];
|
||||
|
||||
export const shapeOptions = [
|
||||
{
|
||||
value: 'pill',
|
||||
label: __( 'Pill', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'rectangle',
|
||||
label: __( 'Rectangle', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
];
|
129
modules/ppcp-settings/resources/js/utils/Helper/ApmButtons.js
Normal file
129
modules/ppcp-settings/resources/js/utils/Helper/ApmButtons.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
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` );
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
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,
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* @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 );
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
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 );
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
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;
|
|
@ -0,0 +1,55 @@
|
|||
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;
|
|
@ -0,0 +1,48 @@
|
|||
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 !== '';
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
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 ] );
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
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;
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
28
modules/ppcp-settings/resources/js/utils/Helper/FormSaver.js
Normal file
28
modules/ppcp-settings/resources/js/utils/Helper/FormSaver.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
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 [];
|
||||
}
|
||||
}
|
92
modules/ppcp-settings/resources/js/utils/Helper/Hiding.js
Normal file
92
modules/ppcp-settings/resources/js/utils/Helper/Hiding.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* @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 );
|
||||
};
|
179
modules/ppcp-settings/resources/js/utils/Helper/LocalStorage.js
Normal file
179
modules/ppcp-settings/resources/js/utils/Helper/LocalStorage.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
/* 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 );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
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;
|
|
@ -0,0 +1,101 @@
|
|||
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 );
|
||||
};
|
196
modules/ppcp-settings/resources/js/utils/Helper/PayerData.js
Normal file
196
modules/ppcp-settings/resources/js/utils/Helper/PayerData.js
Normal file
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* 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 );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 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 );
|
||||
}
|
128
modules/ppcp-settings/resources/js/utils/Helper/ScriptLoading.js
Normal file
128
modules/ppcp-settings/resources/js/utils/Helper/ScriptLoading.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
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 );
|
||||
} );
|
||||
};
|
|
@ -0,0 +1,151 @@
|
|||
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();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
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;
|
25
modules/ppcp-settings/resources/js/utils/Helper/Spinner.js
Normal file
25
modules/ppcp-settings/resources/js/utils/Helper/Spinner.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
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;
|
20
modules/ppcp-settings/resources/js/utils/Helper/Style.js
Normal file
20
modules/ppcp-settings/resources/js/utils/Helper/Style.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
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;
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
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;
|
69
modules/ppcp-settings/resources/js/utils/Helper/Utils.js
Normal file
69
modules/ppcp-settings/resources/js/utils/Helper/Utils.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
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;
|
264
modules/ppcp-settings/resources/js/utils/renderer.js
Normal file
264
modules/ppcp-settings/resources/js/utils/renderer.js
Normal file
|
@ -0,0 +1,264 @@
|
|||
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;
|
179
modules/ppcp-settings/resources/js/utils/widget-builder.js
Normal file
179
modules/ppcp-settings/resources/js/utils/widget-builder.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* 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;
|
|
@ -7,8 +7,26 @@ module.exports = {
|
|||
...{
|
||||
entry: {
|
||||
index: path.resolve( process.cwd(), 'resources/js', 'index.js' ),
|
||||
switchSettingsUi: path.resolve( process.cwd(), 'resources/js', 'switchSettingsUi.js' ),
|
||||
switchSettingsUi: path.resolve(
|
||||
process.cwd(),
|
||||
'resources/js',
|
||||
'switchSettingsUi.js'
|
||||
),
|
||||
style: path.resolve( process.cwd(), 'resources/css', 'style.scss' ),
|
||||
},
|
||||
resolve: {
|
||||
...defaultConfig.resolve,
|
||||
...{
|
||||
alias: {
|
||||
...defaultConfig.resolve.alias,
|
||||
...{
|
||||
ppcpButton: path.resolve(
|
||||
__dirname,
|
||||
'../ppcp-button/resources/js'
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1874,6 +1874,13 @@
|
|||
"@parcel/watcher-win32-ia32" "2.5.0"
|
||||
"@parcel/watcher-win32-x64" "2.5.0"
|
||||
|
||||
"@paypal/paypal-js@^8.1.2":
|
||||
version "8.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.1.2.tgz#3b6199e1c9e3b2a3e444309e0d0ff63556e3ef86"
|
||||
integrity sha512-EKshGSWRxLWU1NyPB9P1TiOkPajVmpTo5I9HuZKoSv8y2uk0XIskXqMkAJ/Y9qAg9iJyP102Jb/atX63tTy24w==
|
||||
dependencies:
|
||||
promise-polyfill "^8.3.0"
|
||||
|
||||
"@pkgr/core@^0.1.0":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
|
||||
|
@ -9365,6 +9372,11 @@ progress@2.0.3, progress@^2.0.3:
|
|||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||
|
||||
promise-polyfill@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
|
||||
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
|
||||
|
||||
prompts@^2.0.1, prompts@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue