Merge trunk

This commit is contained in:
Emili Castells Guasch 2023-08-03 17:20:28 +02:00
commit 2d0726044f
51 changed files with 1480 additions and 688 deletions

View file

@ -0,0 +1,14 @@
name: playwright
repository: julienloizelet/ddev-playwright
version: v2.0.1
install_date: "2023-07-28T14:52:54+02:00"
project_files:
- commands/playwright/playwright
- commands/playwright/playwright-install
- playwright-build/Dockerfile
- playwright-build/kasmvnc.yaml
- playwright-build/xstartup
- playwright-build/entrypoint.sh
- docker-compose.playwright.yaml
global_files: []
removal_actions: []

View file

@ -0,0 +1,14 @@
#!/bin/bash
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
cd /var/www/html || exit 1
cd "${PLAYWRIGHT_TEST_DIR}" || exit 1
export PLAYWRIGHT_BROWSERS_PATH=0
PRE="sudo -u pwuser PLAYWRIGHT_BROWSERS_PATH=0 "
$PRE yarn playwright "$@"

View file

@ -0,0 +1,17 @@
#!/bin/bash
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
cd /var/www/html || exit 1
cd "${PLAYWRIGHT_TEST_DIR}" || exit 1
export PLAYWRIGHT_BROWSERS_PATH=0
PRE="sudo -u pwuser PLAYWRIGHT_BROWSERS_PATH=0 "
$PRE yarn install
$PRE yarn playwright install --with-deps
# Conditionally copy an .env file if an example file exists
[ -f .env.example ] && [ ! -f .env ] && $PRE cp -n .env.example .env; exit 0

View file

@ -0,0 +1,38 @@
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
services:
playwright:
build:
context: playwright-build
container_name: ddev-${DDEV_SITENAME}-playwright
hostname: ${DDEV_SITENAME}-playwright
# These labels ensure this service is discoverable by ddev.
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: $DDEV_APPROOT
environment:
# Modify the PLAYWRIGHT_TEST_DIR folder path to suit your needs
- PLAYWRIGHT_TEST_DIR=tests/Playwright
- NETWORK_IFACE=eth0
- DISPLAY=:1
- VIRTUAL_HOST=$DDEV_HOSTNAME
- HTTP_EXPOSE=8443:8444,9322:9323
- HTTPS_EXPOSE=8444:8444,9323:9323
- DDEV_UID=${DDEV_UID}
- DDEV_GID=${DDEV_GID}
expose:
- "8444"
- "9323"
depends_on:
- web
volumes:
- .:/mnt/ddev_config
- ddev-global-cache:/mnt/ddev-global-cache
- ../:/var/www/html:rw
external_links:
- ddev-router:${DDEV_HOSTNAME}
working_dir: /var/www/html

View file

@ -0,0 +1,57 @@
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
# If on arm64 machine, edit to use mcr.microsoft.com/playwright:focal-arm64
FROM mcr.microsoft.com/playwright:focal
# Debian images by default disable apt caching, so turn it on until we finish
# the build.
RUN mv /etc/apt/apt.conf.d/docker-clean /etc/apt/docker-clean-disabled
USER root
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y sudo
# Give the pwuser user full `sudo` privileges
RUN echo "pwuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/pwuser \
&& chmod 0440 /etc/sudoers.d/pwuser
# CAROOT for `mkcert` to use, has the CA config
ENV CAROOT=/mnt/ddev-global-cache/mkcert
# Install the correct architecture binary of `mkcert`
RUN export TARGETPLATFORM=linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') && mkdir -p /usr/local/bin && curl --fail -JL -s -o /usr/local/bin/mkcert "https://dl.filippo.io/mkcert/latest?for=${TARGETPLATFORM}"
RUN chmod +x /usr/local/bin/mkcert
# Install a window manager.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y icewm xauth
# Install kasmvnc for remote access.
RUN /bin/bash -c 'if [ $(arch) == "aarch64" ]; then KASM_ARCH=arm64; else KASM_ARCH=amd64; fi; wget https://github.com/kasmtech/KasmVNC/releases/download/v1.1.0/kasmvncserver_bullseye_1.1.0_${KASM_ARCH}.deb'
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get install -y ./kasmvncserver*.deb
# We're done with apt so disable caching again for the final image.
RUN mv /etc/apt/docker-clean-disabled /etc/apt/apt.conf.d/docker-clean
# prepare KasmVNC
RUN sudo -u pwuser mkdir /home/pwuser/.vnc
COPY kasmvnc.yaml xstartup /home/pwuser/.vnc/
RUN chown pwuser:pwuser /home/pwuser/.vnc/*
RUN sudo -u pwuser touch /home/pwuser/.vnc/.de-was-selected
RUN sudo -u pwuser /bin/bash -c 'echo -e "secret\nsecret\n" | kasmvncpasswd -wo -u pwuser' # We actually disable auth, but KASM complains without it
COPY entrypoint.sh /root/entrypoint.sh
ENTRYPOINT "/root/entrypoint.sh"

View file

@ -0,0 +1,18 @@
#!/bin/bash
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
# Change pwuser IDs to the host IDs supplied by DDEV
usermod -u ${DDEV_UID} pwuser
groupmod -g ${DDEV_GID} pwuser
usermod -a -G ssl-cert pwuser
# Install DDEV certificate
mkcert -install
# Run CMD from parameters as pwuser
sudo -u pwuser vncserver -fg -disableBasicAuth

View file

@ -0,0 +1,14 @@
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
logging:
log_writer_name: all
log_dest: syslog
level: 100
network:
ssl:
require_ssl: false

32
.ddev/playwright-build/xstartup Executable file
View file

@ -0,0 +1,32 @@
#!/bin/sh
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
export DISPLAY=:1
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
OS=`uname -s`
if [ $OS = 'Linux' ]; then
case "$WINDOWMANAGER" in
*gnome*)
if [ -e /etc/SuSE-release ]; then
PATH=$PATH:/opt/gnome/bin
export PATH
fi
;;
esac
fi
if [ -x /etc/X11/xinit/xinitrc ]; then
exec /etc/X11/xinit/xinitrc
fi
if [ -f /etc/X11/xinit/xinitrc ]; then
exec sh /etc/X11/xinit/xinitrc
fi
[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources
xterm -geometry 80x24+10+10 -ls -title "$VNCDESKTOP Desktop" &
icewm-session

2
.gitignore vendored
View file

@ -11,3 +11,5 @@ modules/ppcp-wc-gateway/assets/css
.env
.env.e2e
auth.json
.DS_Store
tests/.DS_Store

View file

@ -233,9 +233,15 @@ class PayUponInvoiceOrderEndpoint {
* @return array
*/
private function ensure_taxes( WC_Order $wc_order, array $data ): array {
$tax_total = $data['purchase_units'][0]['amount']['breakdown']['tax_total']['value'];
$item_total = $data['purchase_units'][0]['amount']['breakdown']['item_total']['value'];
$shipping = $data['purchase_units'][0]['amount']['breakdown']['shipping']['value'];
$tax_total = $data['purchase_units'][0]['amount']['breakdown']['tax_total']['value'];
$item_total = $data['purchase_units'][0]['amount']['breakdown']['item_total']['value'];
$shipping = $data['purchase_units'][0]['amount']['breakdown']['shipping']['value'];
$handling = isset( $data['purchase_units'][0]['amount']['breakdown']['handling'] ) ? $data['purchase_units'][0]['amount']['breakdown']['handling']['value'] : 0;
$insurance = isset( $data['purchase_units'][0]['amount']['breakdown']['insurance'] ) ? $data['purchase_units'][0]['amount']['breakdown']['insurance']['value'] : 0;
$shipping_discount = isset( $data['purchase_units'][0]['amount']['breakdown']['shipping_discount'] ) ? $data['purchase_units'][0]['amount']['breakdown']['shipping_discount']['value'] : 0;
$discount = isset( $data['purchase_units'][0]['amount']['breakdown']['discount'] ) ? $data['purchase_units'][0]['amount']['breakdown']['discount']['value'] : 0;
$order_tax_total = $wc_order->get_total_tax();
$tax_rate = round( ( $order_tax_total / $item_total ) * 100, 1 );
@ -263,7 +269,7 @@ class PayUponInvoiceOrderEndpoint {
);
$total_amount = $data['purchase_units'][0]['amount']['value'];
$breakdown_total = $item_total + $tax_total + $shipping;
$breakdown_total = $item_total + $tax_total + $shipping + $handling + $insurance - $shipping_discount - $discount;
$diff = round( $total_amount - $breakdown_total, 2 );
if ( $diff === -0.01 || $diff === 0.01 ) {
$data['purchase_units'][0]['amount']['value'] = number_format( $breakdown_total, 2, '.', '' );

View file

@ -13,7 +13,7 @@ import {
ORDER_BUTTON_SELECTOR,
PaymentMethods
} from "./modules/Helper/CheckoutMethodState";
import {hide, setVisible, setVisibleByClass} from "./modules/Helper/Hiding";
import {setVisibleByClass} from "./modules/Helper/Hiding";
import {isChangePaymentPage} from "./modules/Helper/Subscriptions";
import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
import FormSaver from './modules/Helper/FormSaver';
@ -123,7 +123,7 @@ const bootstrap = () => {
return actions.reject();
}
if (context === 'checkout' && !PayPalCommerceGateway.funding_sources_without_redirect.includes(data.fundingSource)) {
if (context === 'checkout') {
try {
await formSaver.save(form);
} catch (error) {
@ -194,9 +194,6 @@ const bootstrap = () => {
payNowBootstrap.init();
}
if (context !== 'checkout') {
messageRenderer.render();
}
};
document.addEventListener(
'DOMContentLoaded',

View file

@ -40,8 +40,7 @@ class SingleProductActionHandler {
}).then((res)=>{
return res.json();
}).then(() => {
const id = document.querySelector('[name="add-to-cart"]').value;
const products = [new Product(id, 1, [])];
const products = this.getSubscriptionProducts();
fetch(this.config.ajax.change_cart.endpoint, {
method: 'POST',
@ -71,6 +70,12 @@ class SingleProductActionHandler {
}
}
getSubscriptionProducts()
{
const id = document.querySelector('[name="add-to-cart"]').value;
return [new Product(id, 1, [])];
}
configuration()
{
return {
@ -98,43 +103,38 @@ class SingleProductActionHandler {
}
}
getProducts()
{
if ( this.isBookingProduct() ) {
const id = document.querySelector('[name="add-to-cart"]').value;
return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"))];
} else if ( this.isGroupedProduct() ) {
const products = [];
this.formElement.querySelectorAll('input[type="number"]').forEach((element) => {
if (! element.value) {
return;
}
const elementName = element.getAttribute('name').match(/quantity\[([\d]*)\]/);
if (elementName.length !== 2) {
return;
}
const id = parseInt(elementName[1]);
const quantity = parseInt(element.value);
products.push(new Product(id, quantity, null));
})
return products;
} else {
const id = document.querySelector('[name="add-to-cart"]').value;
const qty = document.querySelector('[name="quantity"]').value;
const variations = this.variations();
return [new Product(id, qty, variations)];
}
}
createOrder()
{
this.cartHelper = null;
let getProducts = (() => {
if ( this.isBookingProduct() ) {
return () => {
const id = document.querySelector('[name="add-to-cart"]').value;
return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"))];
}
} else if ( this.isGroupedProduct() ) {
return () => {
const products = [];
this.formElement.querySelectorAll('input[type="number"]').forEach((element) => {
if (! element.value) {
return;
}
const elementName = element.getAttribute('name').match(/quantity\[([\d]*)\]/);
if (elementName.length !== 2) {
return;
}
const id = parseInt(elementName[1]);
const quantity = parseInt(element.value);
products.push(new Product(id, quantity, null));
})
return products;
}
} else {
return () => {
const id = document.querySelector('[name="add-to-cart"]').value;
const qty = document.querySelector('[name="quantity"]').value;
const variations = this.variations();
return [new Product(id, qty, variations)];
}
}
})();
return (data, actions) => {
this.errorHandler.clear();
@ -170,7 +170,7 @@ class SingleProductActionHandler {
});
};
return this.updateCart.update(onResolve, getProducts());
return this.updateCart.update(onResolve, this.getProducts());
};
}

View file

@ -1,6 +1,5 @@
import CartActionHandler from '../ActionHandler/CartActionHandler';
import BootstrapHelper from "../Helper/BootstrapHelper";
import {setVisible} from "../Helper/Hiding";
class CartBootstrap {
constructor(gateway, renderer, messages, errorHandler) {
@ -40,11 +39,21 @@ class CartBootstrap {
return;
}
// handle script reload
const newParams = result.data.url_params;
const reloadRequired = this.gateway.url_params.intent !== newParams.intent;
const reloadRequired = JSON.stringify(this.gateway.url_params) !== JSON.stringify(newParams);
// TODO: should reload the script instead
setVisible(this.gateway.button.wrapper, !reloadRequired)
if (reloadRequired) {
this.gateway.url_params = newParams;
jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
}
// handle button status
if (result.data.button || result.data.messages) {
this.gateway.button = result.data.button;
this.gateway.messages = result.data.messages;
this.handleButtonStatus();
}
if (this.lastAmount !== result.data.amount) {
this.lastAmount = result.data.amount;

View file

@ -6,7 +6,6 @@ import {
PaymentMethods
} from "../Helper/CheckoutMethodState";
import BootstrapHelper from "../Helper/BootstrapHelper";
import {disable, enable} from "../Helper/ButtonDisabler";
class CheckoutBootstap {
constructor(gateway, renderer, messages, spinner, errorHandler) {

View file

@ -4,6 +4,8 @@ import {hide, show} from "../Helper/Hiding";
import BootstrapHelper from "../Helper/BootstrapHelper";
import {loadPaypalJsScript} from "../Helper/ScriptLoading";
import {getPlanIdFromVariation} from "../Helper/Subscriptions"
import SimulateCart from "../Helper/SimulateCart";
import {strRemoveWord, strAddWord, throttle} from "../Helper/Utils";
class SingleProductBootstap {
constructor(gateway, renderer, messages, errorHandler) {
@ -14,6 +16,9 @@ class SingleProductBootstap {
this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
this.formSelector = 'form.cart';
// Prevent simulate cart being called too many times in a burst.
this.simulateCartThrottled = throttle(this.simulateCart, 5000);
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
this.handleChange();
}, true);
@ -40,10 +45,14 @@ class SingleProductBootstap {
this.handleButtonStatus();
}
handleButtonStatus() {
handleButtonStatus(simulateCart = true) {
BootstrapHelper.handleButtonStatus(this, {
formSelector: this.formSelector
});
if (simulateCart) {
this.simulateCartThrottled();
}
}
init() {
@ -55,12 +64,6 @@ class SingleProductBootstap {
jQuery(document).on('change', this.formSelector, () => {
this.handleChange();
setTimeout(() => { // Wait for the DOM to be fully updated
// For the moment renderWithAmount should only be done here to prevent undesired side effects due to priceAmount()
// not being correctly formatted in some cases, can be moved to handleButtonStatus() once this issue is fixed
this.messages.renderWithAmount(this.priceAmount());
}, 100);
});
this.mutationObserver.observe(form, { childList: true, subtree: true });
@ -201,6 +204,62 @@ class SingleProductBootstap {
actionHandler.configuration()
);
}
simulateCart() {
const actionHandler = new SingleProductActionHandler(
null,
null,
this.form(),
this.errorHandler,
);
const hasSubscriptions = PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled;
const products = hasSubscriptions
? actionHandler.getSubscriptionProducts()
: actionHandler.getProducts();
(new SimulateCart(
this.gateway.ajax.simulate_cart.endpoint,
this.gateway.ajax.simulate_cart.nonce,
)).simulate((data) => {
this.messages.renderWithAmount(data.total);
let enableFunding = this.gateway.url_params['enable-funding'];
let disableFunding = this.gateway.url_params['disable-funding'];
for (const [fundingSource, funding] of Object.entries(data.funding)) {
if (funding.enabled === true) {
enableFunding = strAddWord(enableFunding, fundingSource);
disableFunding = strRemoveWord(disableFunding, fundingSource);
} else if (funding.enabled === false) {
enableFunding = strRemoveWord(enableFunding, fundingSource);
disableFunding = strAddWord(disableFunding, fundingSource);
}
}
if (
(enableFunding !== this.gateway.url_params['enable-funding']) ||
(disableFunding !== this.gateway.url_params['disable-funding'])
) {
this.gateway.url_params['enable-funding'] = enableFunding;
this.gateway.url_params['disable-funding'] = disableFunding;
jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
}
if (typeof data.button.is_disabled === 'boolean') {
this.gateway.button.is_disabled = data.button.is_disabled;
}
if (typeof data.messages.is_hidden === 'boolean') {
this.gateway.messages.is_hidden = data.messages.is_hidden;
}
this.handleButtonStatus(false);
}, products);
}
}
export default SingleProductBootstap;

View file

@ -1,3 +1,6 @@
import {loadScript} from "@paypal/paypal-js";
import widgetBuilder from "./Renderer/WidgetBuilder";
const storageKey = 'ppcp-data-client-id';
const validateToken = (token, user) => {
@ -24,7 +27,7 @@ const storeToken = (token) => {
sessionStorage.setItem(storageKey, JSON.stringify(token));
}
const dataClientIdAttributeHandler = (script, config) => {
const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
fetch(config.endpoint, {
method: 'POST',
headers: {
@ -42,8 +45,14 @@ const dataClientIdAttributeHandler = (script, config) => {
return;
}
storeToken(data);
script.setAttribute('data-client-token', data.token);
document.body.appendChild(script);
scriptOptions['data-client-token'] = data.token;
loadScript(scriptOptions).then((paypal) => {
if (typeof callback === 'function') {
callback(paypal);
}
});
});
}

View file

@ -1,4 +1,5 @@
import {disable, enable} from "./ButtonDisabler";
import {hide, show} from "./Hiding";
/**
* Common Bootstrap methods to avoid code repetition.
@ -11,20 +12,28 @@ export default class BootstrapHelper {
options.messagesWrapper = options.messagesWrapper || bs.gateway.messages.wrapper;
options.skipMessages = options.skipMessages || false;
if (!bs.shouldEnable()) {
// Handle messages hide / show
if (this.shouldShowMessages(bs, options)) {
show(options.messagesWrapper);
} else {
hide(options.messagesWrapper);
}
// Handle enable / disable
if (bs.shouldEnable()) {
bs.renderer.enableSmartButtons(options.wrapper);
enable(options.wrapper);
if (!options.skipMessages) {
enable(options.messagesWrapper);
}
} else {
bs.renderer.disableSmartButtons(options.wrapper);
disable(options.wrapper, options.formSelector || null);
if (!options.skipMessages) {
disable(options.messagesWrapper);
}
return;
}
bs.renderer.enableSmartButtons(options.wrapper);
enable(options.wrapper);
if (!options.skipMessages) {
enable(options.messagesWrapper);
}
}
@ -37,4 +46,13 @@ export default class BootstrapHelper {
return bs.shouldRender()
&& options.isDisabled !== true;
}
static shouldShowMessages(bs, options) {
options = options || {};
if (typeof options.isMessagesHidden === 'undefined') {
options.isMessagesHidden = bs.gateway.messages.is_hidden;
}
return options.isMessagesHidden !== true;
}
}

View file

@ -1,5 +1,8 @@
import dataClientIdAttributeHandler from "../DataClientIdAttributeHandler";
import {loadScript} from "@paypal/paypal-js";
import widgetBuilder from "../Renderer/WidgetBuilder";
import merge from "deepmerge";
import {keysToCamelCase} from "./Utils";
export const loadPaypalScript = (config, onLoaded) => {
if (typeof paypal !== 'undefined') {
@ -7,21 +10,20 @@ export const loadPaypalScript = (config, onLoaded) => {
return;
}
const script = document.createElement('script');
script.addEventListener('load', onLoaded);
script.setAttribute('src', config.url);
Object.entries(config.script_attributes).forEach(
(keyValue) => {
script.setAttribute(keyValue[0], keyValue[1]);
}
);
const callback = (paypal) => {
widgetBuilder.setPaypal(paypal);
onLoaded();
}
let scriptOptions = keysToCamelCase(config.url_params);
scriptOptions = merge(scriptOptions, config.script_attributes);
if (config.data_client_id.set_attribute) {
dataClientIdAttributeHandler(script, config.data_client_id);
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback);
return;
}
document.body.appendChild(script);
loadScript(scriptOptions).then(callback);
}
export const loadPaypalJsScript = (options, buttons, container) => {

View file

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

View file

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

View file

@ -122,24 +122,17 @@ class CreditCardRenderer {
const className = this._cardNumberFiledCLassNameByCardType(event.cards[0].type);
this._recreateElementClassAttribute(cardNumber, cardNumberField.className);
if (event.fields.number.isValid) {
if (event.cards.length === 1) {
cardNumber.classList.add(className);
}
})
hostedFields.on('validityChange', (event) => {
const formValid = Object.keys(event.fields).every(function (key) {
this.formValid = Object.keys(event.fields).every(function (key) {
return event.fields[key].isValid;
});
const className = event.cards.length ? this._cardNumberFiledCLassNameByCardType(event.cards[0].type) : '';
event.fields.number.isValid
? cardNumber.classList.add(className)
: this._recreateElementClassAttribute(cardNumber, cardNumberField.className);
this.formValid = formValid;
});
hostedFields.on('empty', (event) => {
this._recreateElementClassAttribute(cardNumber, cardNumberField.className);
this.emptyFields.add(event.emittedBy);
});
hostedFields.on('notEmpty', (event) => {

View file

@ -1,3 +1,5 @@
import widgetBuilder from "./WidgetBuilder";
class MessageRenderer {
constructor(config) {
@ -28,7 +30,8 @@ class MessageRenderer {
oldWrapper.parentElement.removeChild(oldWrapper);
sibling.parentElement.insertBefore(newWrapper, sibling);
paypal.Messages(options).render(this.config.wrapper);
widgetBuilder.registerMessages(this.config.wrapper, options);
widgetBuilder.renderMessages(this.config.wrapper);
}
optionsEqual(options) {
@ -44,7 +47,7 @@ class MessageRenderer {
shouldRender() {
if (typeof paypal.Messages === 'undefined' || typeof this.config.wrapper === 'undefined' ) {
if (typeof paypal === 'undefined' || typeof paypal.Messages === 'undefined' || typeof this.config.wrapper === 'undefined' ) {
return false;
}
if (! document.querySelector(this.config.wrapper)) {

View file

@ -1,4 +1,7 @@
import merge from "deepmerge";
import {loadScript} from "@paypal/paypal-js";
import {keysToCamelCase} from "../Helper/Utils";
import widgetBuilder from "./WidgetBuilder";
class Renderer {
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
@ -11,6 +14,8 @@ class Renderer {
this.onButtonsInitListeners = {};
this.renderedSources = new Set();
this.reloadEventName = 'ppcp-reload-buttons';
}
render(contextConfig, settingsOverride = {}, contextConfigOverride = () => {}) {
@ -68,7 +73,9 @@ class Renderer {
}
renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) {
if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) || 'undefined' === typeof paypal.Buttons ) {
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;
}
@ -76,35 +83,50 @@ class Renderer {
contextConfig.fundingSource = fundingSource;
}
const btn = paypal.Buttons({
style,
...contextConfig,
onClick: this.onSmartButtonClick,
onInit: (data, actions) => {
if (this.onSmartButtonsInit) {
this.onSmartButtonsInit(data, actions);
}
this.handleOnButtonsInit(wrapper, data, actions);
},
});
if (!btn.isEligible()) {
return;
const buttonsOptions = () => {
return {
style,
...contextConfig,
onClick: this.onSmartButtonClick,
onInit: (data, actions) => {
if (this.onSmartButtonsInit) {
this.onSmartButtonsInit(data, actions);
}
this.handleOnButtonsInit(wrapper, data, actions);
},
}
}
btn.render(wrapper);
jQuery(document)
.off(this.reloadEventName, wrapper)
.on(this.reloadEventName, wrapper, (event, settingsOverride = {}, triggeredFundingSource) => {
this.renderedSources.add(wrapper + fundingSource ?? '');
// 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]);
}
}
isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) {
// Simply check that has child nodes when we do not need to render buttons separately,
// this will reduce the risk of breaking with different themes/plugins
// and on the cart page (where we also do not need to render separately), which may fully reload this part of the page.
// Ideally we should also find a way to detect such full reloads and remove the corresponding keys from the set.
if (!hasEnabledSeparateGateways) {
return document.querySelector(wrapper).hasChildNodes();
}
return this.renderedSources.has(wrapper + fundingSource ?? '');
isAlreadyRendered(wrapper, fundingSource) {
return this.renderedSources.has(wrapper + (fundingSource ?? ''));
}
disableCreditCardFields() {

View file

@ -0,0 +1,177 @@
/**
* 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;
}
registerButtons(wrapper, options) {
wrapper = this.sanitizeWrapper(wrapper);
this.buttons.set(this.toKey(wrapper), {
wrapper: wrapper,
options: 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;
}
let 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: wrapper,
options: options
});
}
renderMessages(wrapper) {
if (!this.messages.has(wrapper)) {
return;
}
if (this.hasRendered(wrapper)) {
return;
}
const entry = this.messages.get(wrapper);
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 new WidgetBuilder();

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
@ -123,6 +124,17 @@ return array(
'button.request-data' => static function ( ContainerInterface $container ): RequestData {
return new RequestData();
},
'button.endpoint.simulate-cart' => static function ( ContainerInterface $container ): SimulateCartEndpoint {
if ( ! \WC()->cart ) {
throw new RuntimeException( 'cant initialize endpoint at this moment' );
}
$smart_button = $container->get( 'button.smart-button' );
$cart = WC()->cart;
$request_data = $container->get( 'button.request-data' );
$data_store = \WC_Data_Store::load( 'product' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new SimulateCartEndpoint( $smart_button, $cart, $request_data, $data_store, $logger );
},
'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint {
if ( ! \WC()->cart ) {
throw new RuntimeException( 'cant initialize endpoint at this moment' );

View file

@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
@ -683,6 +684,7 @@ class SmartButton implements SmartButtonInterface {
return array(
'wrapper' => '#ppcp-messages',
'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ),
'amount' => $amount,
'placement' => $placement,
'style' => array(
@ -826,9 +828,13 @@ class SmartButton implements SmartButtonInterface {
'has_subscriptions' => $this->has_subscriptions(),
'paypal_subscriptions_enabled' => $this->paypal_subscriptions_enabled(),
),
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'simulate_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ),
),
'change_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
@ -1076,8 +1082,8 @@ class SmartButton implements SmartButtonInterface {
$enable_funding = array( 'venmo' );
if ( $this->settings_status->is_pay_later_button_enabled_for_location( $context ) ||
$this->settings_status->is_pay_later_messaging_enabled_for_location( $context )
if ( $this->is_pay_later_button_enabled_for_location( $context ) ||
$this->is_pay_later_messaging_enabled_for_location( $context )
) {
$enable_funding[] = 'paylater';
} else {
@ -1366,49 +1372,112 @@ class SmartButton implements SmartButtonInterface {
);
}
/**
* Fills and returns the product context_data array to be used in filters.
*
* @param array $context_data The context data for this filter.
* @return array
*/
private function product_filter_context_data( array $context_data = array() ): array {
if ( ! isset( $context_data['product'] ) ) {
$context_data['product'] = wc_get_product();
}
if ( ! $context_data['product'] ) {
return array();
}
if ( ! isset( $context_data['order_total'] ) && ( $context_data['product'] instanceof WC_Product ) ) {
$context_data['order_total'] = (float) $context_data['product']->get_price( 'raw' );
}
return $context_data;
}
/**
* Checks if PayPal buttons/messages should be rendered for the current page.
*
* @param string|null $context The context that should be checked, use default otherwise.
*
* @param array $context_data The context data for this filter.
* @return bool
*/
protected function is_button_disabled( string $context = null ): bool {
public function is_button_disabled( string $context = null, array $context_data = array() ): bool {
if ( null === $context ) {
$context = $this->context();
}
if ( 'product' === $context ) {
$product = wc_get_product();
/**
* Allows to decide if the button should be disabled for a given product
* Allows to decide if the button should be disabled for a given product.
*/
$is_disabled = apply_filters(
return apply_filters(
'woocommerce_paypal_payments_product_buttons_disabled',
null,
$product
false,
$this->product_filter_context_data( $context_data )
);
if ( $is_disabled !== null ) {
return $is_disabled;
}
}
/**
* Allows to decide if the button should be disabled globally or on a given context
* Allows to decide if the button should be disabled globally or on a given context.
*/
$is_disabled = apply_filters(
return apply_filters(
'woocommerce_paypal_payments_buttons_disabled',
null,
false,
$context
);
}
if ( $is_disabled !== null ) {
return $is_disabled;
/**
* Checks a filter if pay_later/messages should be rendered on a given location / context.
*
* @param string $location The location.
* @param array $context_data The context data for this filter.
* @return bool
*/
private function is_pay_later_filter_enabled_for_location( string $location, array $context_data = array() ): bool {
if ( 'product' === $location ) {
/**
* Allows to decide if the button should be disabled for a given product.
*/
return ! apply_filters(
'woocommerce_paypal_payments_product_buttons_paylater_disabled',
false,
$this->product_filter_context_data( $context_data )
);
}
return false;
/**
* Allows to decide if the button should be disabled on a given context.
*/
return ! apply_filters(
'woocommerce_paypal_payments_buttons_paylater_disabled',
false,
$location
);
}
/**
* Check whether Pay Later button is enabled for a given location.
*
* @param string $location The location.
* @param array $context_data The context data for this filter.
* @return bool true if is enabled, otherwise false.
*/
public function is_pay_later_button_enabled_for_location( string $location, array $context_data = array() ): bool {
return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
&& $this->settings_status->is_pay_later_button_enabled_for_location( $location );
}
/**
* Check whether Pay Later message is enabled for a given location.
*
* @param string $location The location setting name.
* @param array $context_data The context data for this filter.
* @return bool true if is enabled, otherwise false.
*/
public function is_pay_later_messaging_enabled_for_location( string $location, array $context_data = array() ): bool {
return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
&& $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
}
/**

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
@ -120,6 +121,19 @@ class ButtonModule implements ModuleInterface {
}
);
add_action(
'wc_ajax_' . SimulateCartEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.simulate-cart' );
/**
* The Simulate Cart Endpoint.
*
* @var SimulateCartEndpoint $endpoint
*/
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . ChangeCartEndpoint::ENDPOINT,
static function () use ( $container ) {

View file

@ -0,0 +1,327 @@
<?php
/**
* Abstract class for cart Endpoints.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
/**
* Abstract Class AbstractCartEndpoint
*/
abstract class AbstractCartEndpoint implements EndpointInterface {
const ENDPOINT = '';
/**
* The current cart object.
*
* @var \WC_Cart
*/
protected $cart;
/**
* The product data store.
*
* @var \WC_Data_Store
*/
protected $product_data_store;
/**
* The request data helper.
*
* @var RequestData
*/
protected $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* The tag to be added to logs.
*
* @var string
*/
protected $logger_tag = '';
/**
* The added cart item IDs
*
* @var array
*/
private $cart_item_keys = array();
/**
* The nonce.
*
* @return string
*/
public static function nonce(): string {
return static::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
return $this->handle_data();
} catch ( Exception $error ) {
$this->logger->error( 'Cart ' . $this->logger_tag . ' failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
'message' => $error->getMessage(),
'code' => $error->getCode(),
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
)
);
return false;
}
}
/**
* Handles the request data.
*
* @return bool
* @throws Exception On error.
*/
abstract protected function handle_data(): bool;
/**
* Adds products to cart.
*
* @param array $products Array of products to be added to cart.
* @return bool
* @throws Exception Add to cart methods throw an exception on fail.
*/
protected function add_products( array $products ): bool {
$this->cart->empty_cart( false );
$success = true;
foreach ( $products as $product ) {
if ( $product['product']->is_type( 'booking' ) ) {
$success = $success && $this->add_booking_product(
$product['product'],
$product['booking']
);
} elseif ( $product['product']->is_type( 'variable' ) ) {
$success = $success && $this->add_variable_product(
$product['product'],
$product['quantity'],
$product['variations']
);
} else {
$success = $success && $this->add_product(
$product['product'],
$product['quantity']
);
}
}
if ( ! $success ) {
$this->handle_error();
}
return $success;
}
/**
* Handles errors.
*
* @return void
*/
private function handle_error(): void {
$message = __(
'Something went wrong. Action aborted',
'woocommerce-paypal-payments'
);
$errors = wc_get_notices( 'error' );
if ( count( $errors ) ) {
$message = array_reduce(
$errors,
static function ( string $add, array $error ): string {
return $add . $error['notice'] . ' ';
},
''
);
wc_clear_notices();
}
wp_send_json_error(
array(
'name' => '',
'message' => $message,
'code' => 0,
'details' => array(),
)
);
}
/**
* Returns product information from request data.
*
* @return array|false
*/
protected function products_from_request() {
$data = $this->request_data->read_request( $this->nonce() );
$products = $this->products_from_data( $data );
if ( ! $products ) {
wp_send_json_error(
array(
'name' => '',
'message' => __(
'Necessary fields not defined. Action aborted.',
'woocommerce-paypal-payments'
),
'code' => 0,
'details' => array(),
)
);
return false;
}
return $products;
}
/**
* Returns product information from a data array.
*
* @param array $data The data array.
*
* @return array|null
*/
protected function products_from_data( array $data ): ?array {
$products = array();
if (
! isset( $data['products'] )
|| ! is_array( $data['products'] )
) {
return null;
}
foreach ( $data['products'] as $product ) {
if ( ! isset( $product['quantity'] ) || ! isset( $product['id'] ) ) {
return null;
}
$wc_product = wc_get_product( (int) $product['id'] );
if ( ! $wc_product ) {
return null;
}
$products[] = array(
'product' => $wc_product,
'quantity' => (int) $product['quantity'],
'variations' => $product['variations'] ?? null,
'booking' => $product['booking'] ?? null,
);
}
return $products;
}
/**
* Adds a product to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_product( \WC_Product $product, int $quantity ): bool {
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), $quantity );
$this->cart_item_keys[] = $cart_item_key;
return false !== $cart_item_key;
}
/**
* Adds variations to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
* @param array $post_variations The variations.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_variable_product(
\WC_Product $product,
int $quantity,
array $post_variations
): bool {
$variations = array();
foreach ( $post_variations as $key => $value ) {
$variations[ $value['name'] ] = $value['value'];
}
$variation_id = $this->product_data_store->find_matching_product_variation( $product, $variations );
// ToDo: Check stock status for variation.
$cart_item_key = $this->cart->add_to_cart(
$product->get_id(),
$quantity,
$variation_id,
$variations
);
$this->cart_item_keys[] = $cart_item_key;
return false !== $cart_item_key;
}
/**
* Adds booking to the cart.
*
* @param \WC_Product $product The Product.
* @param array $data Data used by the booking plugin.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_booking_product(
\WC_Product $product,
array $data
): bool {
if ( ! is_callable( 'wc_bookings_get_posted_data' ) ) {
return false;
}
$cart_item_data = array(
'booking' => wc_bookings_get_posted_data( $data, $product ),
);
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), 1, 0, array(), $cart_item_data );
$this->cart_item_keys[] = $cart_item_key;
return false !== $cart_item_key;
}
/**
* Removes stored cart items from WooCommerce cart.
*
* @return void
*/
protected function remove_cart_items(): void {
foreach ( $this->cart_item_keys as $cart_item_key ) {
$this->cart->remove_cart_item( $cart_item_key );
}
}
}

View file

@ -65,11 +65,17 @@ class CartScriptParamsEndpoint implements EndpointInterface {
*/
public function handle_request(): bool {
try {
if ( is_callable( 'wc_maybe_define_constant' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
$script_data = $this->smart_button->script_data();
wp_send_json_success(
array(
'url_params' => $script_data['url_params'],
'button' => $script_data['button'],
'messages' => $script_data['messages'],
'amount' => WC()->cart->get_total( 'raw' ),
)
);

View file

@ -11,26 +11,15 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
/**
* Class ChangeCartEndpoint
*/
class ChangeCartEndpoint implements EndpointInterface {
class ChangeCartEndpoint extends AbstractCartEndpoint {
const ENDPOINT = 'ppc-change-cart';
/**
* The current cart object.
*
* @var \WC_Cart
*/
private $cart;
/**
* The current shipping object.
*
@ -38,13 +27,6 @@ class ChangeCartEndpoint implements EndpointInterface {
*/
private $shipping;
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The PurchaseUnit factory.
*
@ -52,20 +34,6 @@ class ChangeCartEndpoint implements EndpointInterface {
*/
private $purchase_unit_factory;
/**
* The product data store.
*
* @var \WC_Data_Store
*/
private $product_data_store;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* ChangeCartEndpoint constructor.
*
@ -91,38 +59,8 @@ class ChangeCartEndpoint implements EndpointInterface {
$this->purchase_unit_factory = $purchase_unit_factory;
$this->product_data_store = $product_data_store;
$this->logger = $logger;
}
/**
* The nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
return $this->handle_data();
} catch ( Exception $error ) {
$this->logger->error( 'Cart updating failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
'message' => $error->getMessage(),
'code' => $error->getCode(),
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
)
);
return false;
}
$this->logger_tag = 'updating';
}
/**
@ -131,195 +69,21 @@ class ChangeCartEndpoint implements EndpointInterface {
* @return bool
* @throws Exception On error.
*/
private function handle_data(): bool {
$data = $this->request_data->read_request( $this->nonce() );
$products = $this->products_from_data( $data );
protected function handle_data(): bool {
$products = $this->products_from_request();
if ( ! $products ) {
wp_send_json_error(
array(
'name' => '',
'message' => __(
'Necessary fields not defined. Action aborted.',
'woocommerce-paypal-payments'
),
'code' => 0,
'details' => array(),
)
);
return false;
}
$this->shipping->reset_shipping();
$this->cart->empty_cart( false );
$success = true;
foreach ( $products as $product ) {
if ( $product['product']->is_type( 'booking' ) ) {
$success = $success && $this->add_booking_product(
$product['product'],
$product['booking']
);
} elseif ( $product['product']->is_type( 'variable' ) ) {
$success = $success && $this->add_variable_product(
$product['product'],
$product['quantity'],
$product['variations']
);
} else {
$success = $success && $this->add_product(
$product['product'],
$product['quantity']
);
}
}
if ( ! $success ) {
$this->handle_error();
return $success;
}
wp_send_json_success( $this->generate_purchase_units() );
return $success;
}
/**
* Handles errors.
*
* @return bool
*/
private function handle_error(): bool {
$message = __(
'Something went wrong. Action aborted',
'woocommerce-paypal-payments'
);
$errors = wc_get_notices( 'error' );
if ( count( $errors ) ) {
$message = array_reduce(
$errors,
static function ( string $add, array $error ): string {
return $add . $error['notice'] . ' ';
},
''
);
wc_clear_notices();
}
wp_send_json_error(
array(
'name' => '',
'message' => $message,
'code' => 0,
'details' => array(),
)
);
return true;
}
/**
* Returns product information from an data array.
*
* @param array $data The data array.
*
* @return array|null
*/
private function products_from_data( array $data ) {
$products = array();
if (
! isset( $data['products'] )
|| ! is_array( $data['products'] )
) {
return null;
}
foreach ( $data['products'] as $product ) {
if ( ! isset( $product['quantity'] ) || ! isset( $product['id'] ) ) {
return null;
}
$wc_product = wc_get_product( (int) $product['id'] );
if ( ! $wc_product ) {
return null;
}
$products[] = array(
'product' => $wc_product,
'quantity' => (int) $product['quantity'],
'variations' => $product['variations'] ?? null,
'booking' => $product['booking'] ?? null,
);
}
return $products;
}
/**
* Adds a product to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_product( \WC_Product $product, int $quantity ): bool {
return false !== $this->cart->add_to_cart( $product->get_id(), $quantity );
}
/**
* Adds variations to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
* @param array $post_variations The variations.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_variable_product(
\WC_Product $product,
int $quantity,
array $post_variations
): bool {
$variations = array();
foreach ( $post_variations as $key => $value ) {
$variations[ $value['name'] ] = $value['value'];
}
$variation_id = $this->product_data_store->find_matching_product_variation( $product, $variations );
// ToDo: Check stock status for variation.
return false !== $this->cart->add_to_cart(
$product->get_id(),
$quantity,
$variation_id,
$variations
);
}
/**
* Adds variations to the cart.
*
* @param \WC_Product $product The Product.
* @param array $data Data used by the booking plugin.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_booking_product(
\WC_Product $product,
array $data
): bool {
if ( ! is_callable( 'wc_bookings_get_posted_data' ) ) {
if ( ! $this->add_products( $products ) ) {
return false;
}
$cart_item_data = array(
'booking' => wc_bookings_get_posted_data( $data, $product ),
);
return false !== $this->cart->add_to_cart( $product->get_id(), 1, 0, array(), $cart_item_data );
wp_send_json_success( $this->generate_purchase_units() );
return true;
}
/**

View file

@ -0,0 +1,122 @@
<?php
/**
* Endpoint to simulate adding products to the cart.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
/**
* Class SimulateCartEndpoint
*/
class SimulateCartEndpoint extends AbstractCartEndpoint {
const ENDPOINT = 'ppc-simulate-cart';
/**
* The SmartButton.
*
* @var SmartButton
*/
private $smart_button;
/**
* ChangeCartEndpoint constructor.
*
* @param SmartButton $smart_button The SmartButton.
* @param \WC_Cart $cart The current WC cart object.
* @param RequestData $request_data The request data helper.
* @param \WC_Data_Store $product_data_store The data store for products.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SmartButton $smart_button,
\WC_Cart $cart,
RequestData $request_data,
\WC_Data_Store $product_data_store,
LoggerInterface $logger
) {
$this->smart_button = $smart_button;
$this->cart = clone $cart;
$this->request_data = $request_data;
$this->product_data_store = $product_data_store;
$this->logger = $logger;
$this->logger_tag = 'simulation';
}
/**
* Handles the request data.
*
* @return bool
* @throws Exception On error.
*/
protected function handle_data(): bool {
$products = $this->products_from_request();
if ( ! $products ) {
return false;
}
// Set WC default cart as the clone.
// Store a reference to the real cart.
$active_cart = WC()->cart;
WC()->cart = $this->cart;
if ( ! $this->add_products( $products ) ) {
return false;
}
$this->cart->calculate_totals();
$total = (float) $this->cart->get_total( 'numeric' );
// Remove from cart because some plugins reserve resources internally when adding to cart.
$this->remove_cart_items();
// Restore cart and unset cart clone.
WC()->cart = $active_cart;
unset( $this->cart );
// Process filters.
$pay_later_enabled = true;
$pay_later_messaging_enabled = true;
$button_enabled = true;
foreach ( $products as $product ) {
$context_data = array(
'product' => $product['product'],
'order_total' => $total,
);
$pay_later_enabled = $pay_later_enabled && $this->smart_button->is_pay_later_button_enabled_for_location( 'product', $context_data );
$pay_later_messaging_enabled = $pay_later_messaging_enabled && $this->smart_button->is_pay_later_messaging_enabled_for_location( 'product', $context_data );
$button_enabled = $button_enabled && ! $this->smart_button->is_button_disabled( 'product', $context_data );
}
wp_send_json_success(
array(
'total' => $total,
'funding' => array(
'paylater' => array(
'enabled' => $pay_later_enabled,
),
),
'button' => array(
'is_disabled' => ! $button_enabled,
),
'messages' => array(
'is_hidden' => ! $pay_later_messaging_enabled,
),
)
);
return true;
}
}

View file

@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Onboarding;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
@ -88,9 +87,9 @@ class OnboardingModule implements ModuleInterface {
$endpoint = $c->get( 'onboarding.endpoint.login-seller' );
/**
* The ChangeCartEndpoint.
* The LoginSellerEndpoint.
*
* @var ChangeCartEndpoint $endpoint
* @var LoginSellerEndpoint $endpoint
*/
$endpoint->handle_request();
}

View file

@ -2,9 +2,10 @@ import { loadScript } from "@paypal/paypal-js";
import {debounce} from "./helper/debounce";
import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Renderer'
import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer";
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding"
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
;document.addEventListener(
document.addEventListener(
'DOMContentLoaded',
() => {
function disableAll(nodeList){
@ -138,6 +139,8 @@ import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/mo
function loadPaypalScript(settings, onLoaded = () => {}) {
loadScript(JSON.parse(JSON.stringify(settings))) // clone the object to prevent modification
.then(paypal => {
widgetBuilder.setPaypal(paypal);
document.dispatchEvent(new CustomEvent('ppcp_paypal_script_loaded'));
onLoaded(paypal);

View file

@ -145,6 +145,10 @@ class CheckoutOrderApproved implements RequestHandler {
return $this->success_response();
}
if ( ! (bool) apply_filters( 'woocommerce_paypal_payments_order_approved_webhook_can_create_wc_order', false ) ) {
return $this->success_response();
}
$wc_session = new WC_Session_Handler();
$session_data = $wc_session->get_session( $customer_id );
@ -159,6 +163,14 @@ class CheckoutOrderApproved implements RequestHandler {
WC()->cart->calculate_shipping();
$form = $this->session_handler->checkout_form();
if ( ! $form ) {
return $this->failure_response(
sprintf(
'Failed to create WC order in webhook event %s, checkout data not found.',
$request['id'] ?: ''
)
);
}
$checkout = new WC_Checkout();
$wc_order_id = $checkout->create_order( $form );

View file

@ -12,7 +12,6 @@
"install:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn install",
"install:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn install",
"install:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn install",
"install:modules:ppcp-subscription": "cd modules/ppcp-subscription && yarn install",
"install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
"install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install",
@ -21,7 +20,6 @@
"build:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run build",
"build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build",
"build:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run build",
"build:modules:ppcp-subscription": "cd modules/ppcp-subscription && yarn run build",
"build:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run build",
"build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build",
"build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build",
@ -31,7 +29,6 @@
"watch:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run watch",
"watch:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run watch",
"watch:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run watch",
"watch:modules:ppcp-subscription": "cd modules/ppcp-subscription && yarn run watch",
"watch:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run watch",
"watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch",
"watch:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run watch",
@ -72,9 +69,5 @@
"dependencies": {
"dotenv": "^16.0.3",
"npm-run-all": "^4.1.5"
},
"devDependencies": {
"@playwright/test": "^1.31.1",
"@woocommerce/woocommerce-rest-api": "^1.0.1"
}
}

View file

@ -1,12 +0,0 @@
require('dotenv').config({ path: '.env.e2e' });
const config = {
testDir: './tests/playwright',
timeout: 30000,
use: {
baseURL: process.env.BASEURL,
ignoreHTTPSErrors: true,
},
};
module.exports = config;

View file

@ -0,0 +1,36 @@
BASEURL="https://woocommerce-paypal-payments.ddev.site"
AUTHORIZATION="Bearer ABC123"
CHECKOUT_URL="/checkout"
CHECKOUT_PAGE_ID=7
CART_URL="/cart"
BLOCK_CHECKOUT_URL="/checkout-block"
BLOCK_CHECKOUT_PAGE_ID=22
BLOCK_CART_URL="/cart-block"
PRODUCT_URL="/product/prod"
PRODUCT_ID=123
SUBSCRIPTION_URL="/product/sub"
APM_ID="sofort"
WP_MERCHANT_USER="admin"
WP_MERCHANT_PASSWORD="admin"
WP_CUSTOMER_USER="customer"
WP_CUSTOMER_PASSWORD="password"
CUSTOMER_EMAIL="customer@example.com"
CUSTOMER_PASSWORD="password"
CUSTOMER_FIRST_NAME="John"
CUSTOMER_LAST_NAME="Doe"
CUSTOMER_COUNTRY="DE"
CUSTOMER_ADDRESS="street 1"
CUSTOMER_POSTCODE="12345"
CUSTOMER_CITY="city"
CUSTOMER_PHONE="1234567890"
CREDIT_CARD_NUMBER="1234567890"
CREDIT_CARD_EXPIRATION="01/2042"
CREDIT_CARD_CVV="123"

6
tests/Playwright/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules
pw-browsers
test-results
.env
yarn.lock
playwright-report/*

View file

@ -0,0 +1,22 @@
## ddev-playwright addon
https://github.com/julienloizelet/ddev-playwright
### Install
```
$ ddev restart
$ ddev playwright-install
```
### Usage
https://github.com/julienloizelet/ddev-playwright#basic-usage
```
$ ddev playwright test
```
### Known issues
It does not open browser in macOS, to make it work use `npx`:
```
$ cd tests/Playwright
$ npx playwright test
```

View file

@ -0,0 +1,7 @@
{
"license": "MIT",
"dependencies": {
"@playwright/test": "^1.34.2",
"dotenv": "^16.0.3"
}
}

View file

@ -0,0 +1,77 @@
// @ts-check
const { defineConfig, devices } = require('@playwright/test');
require('dotenv').config({ path: '.env' });
/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
[process.env.CI ? 'github' : 'list'],
['html', {open: 'never'}],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.BASEURL,
ignoreHTTPSErrors: true,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
//
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View file

@ -1,3 +0,0 @@
test-results/
playwright-report/
.cache/

View file

@ -1,40 +0,0 @@
# Playwright Testing
## Set Environment Variables
Duplicate [.env.e2e.example](/.env.e2e.example) and rename it to `.env.e2e`, set the values needed for the tests, like account credentials, card numbers.
## Install Playwright dependencies (browsers)
```
$ yarn ddev:pw-install
```
## Run Tests
```
$ yarn ddev:pw-tests
```
You can also choose which tests to run filtering by name
```
$ yarn ddev:pw-tests --grep "Test name or part of the name"
```
Or run without the headless mode (show the browser)
```
$ yarn pw-tests-headed
```
Or run with [the test debugger](https://playwright.dev/docs/debug)
```
$ yarn playwright test --debug
```
For the headed/debug mode (currently works only outside DDEV) you may need to re-install the deps if you are not on Linux.
```
$ yarn pw-install
```
---
See [Playwright docs](https://playwright.dev/docs/intro) for more info.

View file

@ -1,59 +0,0 @@
import {loginAsAdmin} from "./user";
import {expect} from "@playwright/test";
const wcApi = require('@woocommerce/woocommerce-rest-api').default;
const {
BASEURL,
WP_MERCHANT_USER,
WP_MERCHANT_PASSWORD,
} = process.env;
const wc = () => {
return new wcApi({
url: BASEURL,
consumerKey: WP_MERCHANT_USER,
consumerSecret: WP_MERCHANT_PASSWORD,
version: 'wc/v3',
});
}
export const createProduct = async (data) => {
const api = wc();
return await api.post('products', data)
.then((response) => {
return response.data.id
}).catch((error) => {
console.log(error)
})
}
export const updateProduct = async (data, id) => {
const api = wc();
return await api.put(`products/${id}`, data)
.then((response) => {
return response.data.id
}).catch((error) => {
console.log(error)
})
}
export const deleteProduct = async (id) => {
const api = wc();
return await api.delete(`products/${id}`)
.then((response) => {
return response.data.id
}).catch((error) => {
console.log(error)
})
}
export const updateProductUi = async (id, page) => {
await loginAsAdmin(page);
await page.goto(`/wp-admin/post.php?post=${id}&action=edit`)
await page.locator('#publish').click();
await expect(page.getByText('Product updated.')).toBeVisible();
}

180
yarn.lock
View file

@ -2,31 +2,6 @@
# yarn lockfile v1
"@playwright/test@^1.31.1":
version "1.31.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.31.1.tgz#39d6873dc46af135f12451d79707db7d1357455d"
integrity sha512-IsytVZ+0QLDh1Hj83XatGp/GsI1CDJWbyDaBGbainsh0p2zC7F4toUocqowmjS6sQff2NGT3D9WbDj/3K2CJiA==
dependencies:
"@types/node" "*"
playwright-core "1.31.1"
optionalDependencies:
fsevents "2.3.2"
"@types/node@*":
version "18.14.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.1.tgz#90dad8476f1e42797c49d6f8b69aaf9f876fc69f"
integrity sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==
"@woocommerce/woocommerce-rest-api@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@woocommerce/woocommerce-rest-api/-/woocommerce-rest-api-1.0.1.tgz#112b998e8e3758203a71d5e3e0b24efcb1cff842"
integrity sha512-YBk3EEYE0zax/egx6Rhpbu6hcCFyZpYQrjH9JO4NUGU3n3T0W9Edn7oAUbjL/c7Oezcg+UaQluCaKjY/B3zwxg==
dependencies:
axios "^0.19.0"
create-hmac "^1.1.7"
oauth-1.0a "^2.2.6"
url-parse "^1.4.7"
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@ -34,13 +9,6 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
axios@^0.19.0:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
dependencies:
follow-redirects "1.5.10"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -71,14 +39,6 @@ chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
dependencies:
inherits "^2.0.1"
safe-buffer "^5.0.1"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -96,29 +56,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
create-hash@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
dependencies:
cipher-base "^1.0.1"
inherits "^2.0.1"
md5.js "^1.3.4"
ripemd160 "^2.0.1"
sha.js "^2.4.0"
create-hmac@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
dependencies:
cipher-base "^1.0.3"
create-hash "^1.1.0"
inherits "^2.0.1"
ripemd160 "^2.0.0"
safe-buffer "^5.0.1"
sha.js "^2.4.8"
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -130,13 +67,6 @@ cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
define-properties@^1.1.3, define-properties@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
@ -201,18 +131,6 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@ -291,25 +209,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hash-base@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
dependencies:
inherits "^2.0.4"
readable-stream "^3.6.0"
safe-buffer "^5.2.0"
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
internal-slot@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@ -426,15 +330,6 @@ load-json-file@^4.0.0:
pify "^3.0.0"
strip-bom "^3.0.0"
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
dependencies:
hash-base "^3.0.0"
inherits "^2.0.1"
safe-buffer "^5.1.2"
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
@ -447,11 +342,6 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@ -482,11 +372,6 @@ npm-run-all@^4.1.5:
shell-quote "^1.6.1"
string.prototype.padend "^3.0.0"
oauth-1.0a@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz#eadbccdb3bceea412d24586e6f39b2b412f0e491"
integrity sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==
object-inspect@^1.12.2, object-inspect@^1.9.0:
version "1.12.2"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
@ -542,16 +427,6 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
playwright-core@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.1.tgz#4deeebbb8fb73b512593fe24bea206d8fd85ff7f"
integrity sha512-JTyX4kV3/LXsvpHkLzL2I36aCdml4zeE35x+G5aPc4bkLsiRiQshU5lWeVpHFAuC8xAcbI6FDcw/8z3q2xtJSQ==
querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
@ -561,15 +436,6 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2"
path-type "^3.0.0"
readable-stream@^3.6.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
regexp.prototype.flags@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
@ -579,11 +445,6 @@ regexp.prototype.flags@^1.4.3:
define-properties "^1.1.3"
functions-have-names "^1.2.2"
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
resolve@^1.10.0:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
@ -593,19 +454,6 @@ resolve@^1.10.0:
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
dependencies:
hash-base "^3.0.0"
inherits "^2.0.1"
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-regex-test@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295"
@ -620,14 +468,6 @@ safe-regex-test@^1.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
sha.js@^2.4.0, sha.js@^2.4.8:
version "2.4.11"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
dependencies:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -707,13 +547,6 @@ string.prototype.trimstart@^1.0.5:
define-properties "^1.1.4"
es-abstract "^1.19.5"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@ -741,19 +574,6 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
url-parse@^1.4.7:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"