Merge branch 'refs/heads/trunk' into modularity-module-migration

# Conflicts:
#	modules/ppcp-applepay/src/ApplepayModule.php
#	modules/ppcp-compat/src/CompatModule.php
This commit is contained in:
Moritz Meißelbach 2024-08-23 15:50:49 +02:00
commit 789d979c80
No known key found for this signature in database
GPG key ID: 9FDCE7BEB31FA3E5
113 changed files with 11616 additions and 2360 deletions

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ClientCredentials;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
@ -240,6 +241,13 @@ return array(
$bn_code
);
},
'api.endpoint.orders' => static function ( ContainerInterface $container ): Orders {
return new Orders(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint {
return new BillingAgreementsEndpoint(
$container->get( 'api.host' ),
@ -1444,7 +1452,6 @@ return array(
'CN' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array(),
),
'CY' => array(
'mastercard' => array(),

View file

@ -0,0 +1,108 @@
<?php
/**
* Orders API endpoints.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
* @link https://developer.paypal.com/docs/api/orders/v2/ Orders API documentation.
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WP_Error;
/**
* Class Orders
*/
class Orders {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Orders constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Creates a PayPal order.
*
* @param array $request_body The request body.
* @param array $headers The request headers.
* @return array
* @throws RuntimeException If something went wrong with the request.
* @throws PayPalApiException If something went wrong with the PayPal API request.
*/
public function create( array $request_body, array $headers = array() ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders';
$default_headers = array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
);
$headers = array_merge(
$default_headers,
$headers
);
$args = array(
'method' => 'POST',
'headers' => $headers,
'body' => wp_json_encode( $request_body ),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
throw new PayPalApiException(
json_decode( $response['body'] ),
$status_code
);
}
return $response;
}
}

View file

@ -269,7 +269,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'default' => 'plain',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',

View file

@ -52,3 +52,7 @@
}
}
}
#ppc-button-ppcp-applepay {
display: none;
}

View file

@ -1,3 +1,6 @@
/* global ApplePaySession */
/* global PayPalCommerceGateway */
import ContextHandlerFactory from './Context/ContextHandlerFactory';
import { createAppleErrors } from './Helper/applePayError';
import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
@ -7,18 +10,95 @@ import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
class ApplepayButton {
constructor( context, externalHandler, buttonConfig, ppcpConfig ) {
apmButtonsInit( ppcpConfig );
/**
* Plugin-specific styling.
*
* Note that most properties of this object do not apply to the Apple Pay button.
*
* @typedef {Object} PPCPStyle
* @property {string} shape - Outline shape.
* @property {?number} height - Button height in pixel.
*/
this.isInitialized = false;
/**
* Style options that are defined by the Apple Pay SDK and are required to render the button.
*
* @typedef {Object} ApplePayStyle
* @property {string} type - Defines the button label.
* @property {string} color - Button color
* @property {string} lang - The locale; an empty string will apply the user-agent's language.
*/
/**
* List of valid context values that the button can have.
*
* @type {Object}
*/
const CONTEXT = {
Product: 'product',
Cart: 'cart',
Checkout: 'checkout',
PayNow: 'pay-now',
MiniCart: 'mini-cart',
BlockCart: 'cart-block',
BlockCheckout: 'checkout-block',
Preview: 'preview',
// Block editor contexts.
Blocks: [ 'cart-block', 'checkout-block' ],
// Custom gateway contexts.
Gateways: [ 'checkout', 'pay-now' ],
};
/**
* A payment button for Apple Pay.
*
* On a single page, multiple Apple Pay buttons can be displayed, which also means multiple
* ApplePayButton instances exist. A typical case is on the product page, where one Apple Pay button
* is located inside the minicart-popup, and another pay-now button is in the product context.
*/
class ApplePayButton {
/**
* Whether the payment button is initialized.
*
* @type {boolean}
*/
#isInitialized = false;
#wrapperId = '';
#ppcpButtonWrapperId = '';
/**
* Context describes the button's location on the website and what details it submits.
*
* @type {''|'product'|'cart'|'checkout'|'pay-now'|'mini-cart'|'cart-block'|'checkout-block'|'preview'}
*/
context = '';
externalHandler = null;
buttonConfig = null;
ppcpConfig = null;
paymentsClient = null;
formData = null;
contextHandler = null;
updatedContactInfo = [];
selectedShippingMethod = [];
/**
* Stores initialization data sent to the button.
*/
initialPaymentRequest = null;
constructor( context, externalHandler, buttonConfig, ppcpConfig ) {
this._initDebug( !! buttonConfig?.is_debug );
apmButtonsInit( ppcpConfig );
this.context = context;
this.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.paymentsClient = null;
this.formData = null;
this.contextHandler = ContextHandlerFactory.create(
this.context,
@ -26,36 +106,226 @@ class ApplepayButton {
this.ppcpConfig
);
this.updatedContactInfo = [];
this.selectedShippingMethod = [];
this.nonce =
document.getElementById( 'woocommerce-process-checkout-nonce' )
?.value || buttonConfig.nonce;
// Stores initialization data sent to the button.
this.initialPaymentRequest = null;
// Default eligibility status.
this.isEligible = true;
this.log = function () {
if ( this.buttonConfig.is_debug ) {
//console.log('[ApplePayButton]', ...arguments);
}
};
this.refreshContextData();
}
/**
* NOOP log function to avoid errors when debugging is disabled.
*/
log() {}
/**
* Enables debugging tools, when the button's is_debug flag is set.
*
* @param {boolean} enableDebugging If debugging features should be enabled for this instance.
* @private
*/
_initDebug( enableDebugging ) {
if ( ! enableDebugging || this.#isInitialized ) {
return;
}
// Debug helpers
jQuery( document ).on( 'ppcp-applepay-debug', () => {
console.log( 'ApplePayButton', this.context, this );
} );
document.ppcpApplepayButtons = document.ppcpApplepayButtons || {};
document.ppcpApplepayButtons[ this.context ] = this;
this.log = ( ...args ) => {
console.log( `[ApplePayButton | ${ this.context }]`, ...args );
};
jQuery( document ).on( 'ppcp-applepay-debug', () => {
this.log( this );
} );
}
/**
* The nonce for ajax requests.
*
* @return {string} The nonce value
*/
get nonce() {
const input = document.getElementById(
'woocommerce-process-checkout-nonce'
);
return input?.value || this.buttonConfig.nonce;
}
/**
* Whether the current page qualifies to use the Apple Pay button.
*
* In admin, the button is always eligible, to display an accurate preview.
* On front-end, PayPal's response decides if customers can use Apple Pay.
*
* @return {boolean} True, if the button can be displayed.
*/
get isEligible() {
if ( ! this.#isInitialized ) {
return true;
}
if ( CONTEXT.Preview === this.context ) {
return true;
}
/**
* Ensure the ApplePaySession is available and accepts payments
* This check is required when using Apple Pay SDK v1; canMakePayments() returns false
* if the current device is not liked to iCloud or the Apple Wallet is not available
* for a different reason.
*/
try {
if ( ! window.ApplePaySession?.canMakePayments() ) {
return false;
}
} catch ( error ) {
console.warn( error );
return false;
}
return !! this.applePayConfig.isEligible;
}
/**
* Determines if the current payment button should be rendered as a stand-alone gateway.
* The return value `false` usually means, that the payment button is bundled with all available
* payment buttons.
*
* The decision depends on the button context (placement) and the plugin settings.
*
* @return {boolean} True, if the current button represents a stand-alone gateway.
*/
get isSeparateGateway() {
return (
this.buttonConfig.is_wc_gateway_enabled &&
CONTEXT.Gateways.includes( this.context )
);
}
/**
* Returns the wrapper ID for the current button context.
* The ID varies for the MiniCart context.
*
* @return {string} The wrapper-element's ID (without the `#` prefix).
*/
get wrapperId() {
if ( ! this.#wrapperId ) {
let id;
if ( CONTEXT.MiniCart === this.context ) {
id = this.buttonConfig.button.mini_cart_wrapper;
} else if ( this.isSeparateGateway ) {
id = 'ppc-button-ppcp-applepay';
} else {
id = this.buttonConfig.button.wrapper;
}
this.#wrapperId = id.replace( /^#/, '' );
}
return this.#wrapperId;
}
/**
* Returns the wrapper ID for the ppcpButton
*
* @return {string} The wrapper-element's ID (without the `#` prefix).
*/
get ppcpButtonWrapperId() {
if ( ! this.#ppcpButtonWrapperId ) {
let id;
if ( CONTEXT.MiniCart === this.context ) {
id = this.ppcpConfig.button.mini_cart_wrapper;
} else if ( CONTEXT.Blocks.includes( this.context ) ) {
id = '#express-payment-method-ppcp-gateway-paypal';
} else {
id = this.ppcpConfig.button.wrapper;
}
this.#ppcpButtonWrapperId = id.replace( /^#/, '' );
}
return this.#ppcpButtonWrapperId;
}
/**
* Returns the context-relevant PPCP style object.
* The style for the MiniCart context can be different.
*
* The PPCP style are custom style options, that are provided by this plugin.
*
* @return {PPCPStyle} The style object.
*/
get ppcpStyle() {
if ( CONTEXT.MiniCart === this.context ) {
return this.ppcpConfig.button.mini_cart_style;
}
return this.ppcpConfig.button.style;
}
/**
* Returns default style options that are propagated to and rendered by the Apple Pay button.
*
* These styles are the official style options provided by the Apple Pay SDK.
*
* @return {ApplePayStyle} The style object.
*/
get buttonStyle() {
return {
type: this.buttonConfig.button.type,
lang: this.buttonConfig.button.lang,
color: this.buttonConfig.button.color,
};
}
/**
* Returns the HTML element that wraps the current button
*
* @return {HTMLElement|null} The wrapper element, or null.
*/
get wrapperElement() {
return document.getElementById( this.wrapperId );
}
/**
* Returns an array of HTMLElements that belong to the payment button.
*
* @return {HTMLElement[]} List of payment button wrapper elements.
*/
get allElements() {
const selectors = [];
// Payment button (Pay now, smart button block)
selectors.push( `#${ this.wrapperId }` );
// Block Checkout: Express checkout button.
if ( CONTEXT.Blocks.includes( this.context ) ) {
selectors.push( '#express-payment-method-ppcp-applepay' );
}
// Classic Checkout: Apple Pay gateway.
if ( CONTEXT.Gateways.includes( this.context ) ) {
selectors.push( '.wc_payment_method.payment_method_ppcp-applepay' );
}
this.log( 'Wrapper Elements:', selectors );
return /** @type {HTMLElement[]} */ selectors.flatMap( ( selector ) =>
Array.from( document.querySelectorAll( selector ) )
);
}
/**
* Checks whether the main button-wrapper is present in the current DOM.
*
* @return {boolean} True, if the button context (wrapper element) is found.
*/
get isPresent() {
return this.wrapperElement instanceof HTMLElement;
}
init( config ) {
if ( this.isInitialized ) {
if ( this.#isInitialized ) {
return;
}
@ -63,41 +333,35 @@ class ApplepayButton {
return;
}
this.log( 'Init', this.context );
this.log( 'Init' );
this.initEventHandlers();
this.isInitialized = true;
this.#isInitialized = true;
this.applePayConfig = config;
this.isEligible =
( this.applePayConfig.isEligible && window.ApplePaySession ) ||
this.buttonConfig.is_admin;
if ( this.isEligible ) {
this.fetchTransactionInfo().then( () => {
this.addButton();
const id_minicart =
'#apple-' + this.buttonConfig.button.mini_cart_wrapper;
const id = '#apple-' + this.buttonConfig.button.wrapper;
if ( this.context === 'mini-cart' ) {
document
.querySelector( id_minicart )
?.addEventListener( 'click', ( evt ) => {
evt.preventDefault();
this.onButtonClick();
} );
} else {
document
.querySelector( id )
?.addEventListener( 'click', ( evt ) => {
evt.preventDefault();
this.onButtonClick();
} );
}
} );
if ( ! this.isEligible ) {
this.hide();
} else {
jQuery( '#' + this.buttonConfig.button.wrapper ).hide();
jQuery( '#' + this.buttonConfig.button.mini_cart_wrapper ).hide();
jQuery( '#express-payment-method-ppcp-applepay' ).hide();
// Bail if the button wrapper is not present; handles mini-cart logic on checkout page.
if ( ! this.isPresent ) {
this.log( 'Abort init (no wrapper found)' );
return;
}
this.show();
this.fetchTransactionInfo().then( () => {
const button = this.addButton();
if ( ! button ) {
return;
}
button.addEventListener( 'click', ( evt ) => {
evt.preventDefault();
this.onButtonClick();
} );
} );
}
}
@ -106,49 +370,51 @@ class ApplepayButton {
return;
}
this.isInitialized = false;
this.#isInitialized = false;
this.init( this.applePayConfig );
}
/**
* Hides all wrappers that belong to this ApplePayButton instance.
*/
hide() {
this.log( 'Hide button' );
this.allElements.forEach( ( element ) => {
element.style.display = 'none';
} );
}
/**
* Ensures all wrapper elements of this ApplePayButton instance are visible.
*/
show() {
this.log( 'Show button' );
if ( ! this.isPresent ) {
this.log( '!! Cannot show button, wrapper is not present' );
return;
}
// Classic Checkout/PayNow: Make the Apple Pay gateway visible after page load.
document
.querySelectorAll( 'style#ppcp-hide-apple-pay' )
.forEach( ( el ) => el.remove() );
this.allElements.forEach( ( element ) => {
element.style.display = '';
} );
}
async fetchTransactionInfo() {
this.transactionInfo = await this.contextHandler.transactionInfo();
}
/**
* Returns configurations relative to this button context.
*/
contextConfig() {
const config = {
wrapper: this.buttonConfig.button.wrapper,
ppcpStyle: this.ppcpConfig.button.style,
buttonStyle: this.buttonConfig.button.style,
ppcpButtonWrapper: this.ppcpConfig.button.wrapper,
};
if ( this.context === 'mini-cart' ) {
config.wrapper = this.buttonConfig.button.mini_cart_wrapper;
config.ppcpStyle = this.ppcpConfig.button.mini_cart_style;
config.buttonStyle = this.buttonConfig.button.mini_cart_style;
config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper;
}
if (
[ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1
) {
config.ppcpButtonWrapper =
'#express-payment-method-ppcp-gateway-paypal';
}
return config;
}
initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
const wrapper_id = '#' + wrapper;
const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
const wrapperId = `#${ this.wrapperId }`;
if ( wrapper_id === ppcpButtonWrapper ) {
if ( wrapperId === ppcpButtonWrapper ) {
throw new Error(
`[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper_id }"`
`[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapperId }"`
);
}
@ -158,9 +424,9 @@ class ApplepayButton {
}
const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
setVisible( wrapper_id, $ppcpButtonWrapper.is( ':visible' ) );
setVisible( wrapperId, $ppcpButtonWrapper.is( ':visible' ) );
setEnabled(
wrapper_id,
wrapperId,
! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
);
};
@ -178,8 +444,9 @@ class ApplepayButton {
}
/**
* Starts an ApplePay session.
* @param paymentRequest
* Starts an Apple Pay session.
*
* @param {Object} paymentRequest The payment request object.
*/
applePaySession( paymentRequest ) {
this.log( 'applePaySession', paymentRequest );
@ -192,6 +459,7 @@ class ApplepayButton {
session.onshippingcontactselected =
this.onShippingContactSelected( session );
}
session.onvalidatemerchant = this.onValidateMerchant( session );
session.onpaymentauthorized = this.onPaymentAuthorized( session );
return session;
@ -199,32 +467,38 @@ class ApplepayButton {
/**
* Adds an Apple Pay purchase button.
*
* @return {HTMLElement|null} The newly created `<apple-pay-button>` element. Null on failure.
*/
addButton() {
this.log( 'addButton', this.context );
this.log( 'addButton' );
const { wrapper, ppcpStyle } = this.contextConfig();
const wrapper = this.wrapperElement;
const style = this.buttonStyle;
const id = 'apple-' + this.wrapperId;
const appleContainer = document.getElementById( wrapper );
const type = this.buttonConfig.button.type;
const language = this.buttonConfig.button.lang;
const color = this.buttonConfig.button.color;
const id = 'apple-' + wrapper;
if ( appleContainer ) {
appleContainer.innerHTML = `<apple-pay-button id="${ id }" buttonstyle="${ color }" type="${ type }" locale="${ language }">`;
if ( ! wrapper ) {
return null;
}
const $wrapper = jQuery( '#' + wrapper );
$wrapper.addClass( 'ppcp-button-' + ppcpStyle.shape );
const ppcpStyle = this.ppcpStyle;
wrapper.innerHTML = `<apple-pay-button id='${ id }' buttonstyle='${ style.color }' type='${ style.type }' locale='${ style.lang }' />`;
wrapper.classList.add(
`ppcp-button-${ ppcpStyle.shape }`,
'ppcp-button-apm',
'ppcp-button-applepay'
);
if ( ppcpStyle.height ) {
$wrapper.css(
wrapper.style.setProperty(
'--apple-pay-button-height',
`${ ppcpStyle.height }px`
);
$wrapper.css( 'height', `${ ppcpStyle.height }px` );
wrapper.style.height = `${ ppcpStyle.height }px`;
}
return wrapper.querySelector( 'apple-pay-button' );
}
//------------------------
@ -235,19 +509,21 @@ class ApplepayButton {
* Show Apple Pay payment sheet when Apple Pay payment button is clicked
*/
async onButtonClick() {
this.log( 'onButtonClick', this.context );
this.log( 'onButtonClick' );
const paymentRequest = this.paymentRequest();
window.ppcpFundingSource = 'apple_pay'; // Do this on another place like on create order endpoint handler.
// Do this on another place like on create order endpoint handler.
window.ppcpFundingSource = 'apple_pay';
// Trigger woocommerce validation if we are in the checkout page.
if ( this.context === 'checkout' ) {
if ( CONTEXT.Checkout === this.context ) {
const checkoutFormSelector = 'form.woocommerce-checkout';
const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic,
document.querySelector( '.woocommerce-notices-wrapper' )
);
try {
const formData = new FormData(
document.querySelector( checkoutFormSelector )
@ -269,6 +545,7 @@ class ApplepayButton {
PayPalCommerceGateway.ajax.validate_checkout.nonce
)
: null;
if ( formValidator ) {
try {
const errors = await formValidator.validate(
@ -296,13 +573,13 @@ class ApplepayButton {
/**
* If the button should show the shipping fields.
*
* @return {false|*}
* @return {boolean} True, if shipping fields should be captured by ApplePay.
*/
shouldRequireShippingInButton() {
return (
this.contextHandler.shippingAllowed() &&
this.buttonConfig.product.needShipping &&
( this.context !== 'checkout' ||
( CONTEXT.Checkout !== this.context ||
this.shouldUpdateButtonWithFormData() )
);
}
@ -310,10 +587,10 @@ class ApplepayButton {
/**
* If the button should be updated with the form addresses.
*
* @return {boolean}
* @return {boolean} True, when Apple Pay data should be submitted to WooCommerce.
*/
shouldUpdateButtonWithFormData() {
if ( this.context !== 'checkout' ) {
if ( CONTEXT.Checkout !== this.context ) {
return false;
}
return (
@ -323,29 +600,28 @@ class ApplepayButton {
}
/**
* Indicates how payment completion should be handled if with the context handler default actions.
* Or with ApplePay module specific completion.
* Indicates how payment completion should be handled if with the context handler default
* actions. Or with Apple Pay module specific completion.
*
* @return {boolean}
* @return {boolean} True, when the Apple Pay data should be submitted to WooCommerce.
*/
shouldCompletePaymentWithContextHandler() {
// Data already handled, ex: PayNow
if ( ! this.contextHandler.shippingAllowed() ) {
return true;
}
// Use WC form data mode in Checkout.
if (
this.context === 'checkout' &&
return (
CONTEXT.Checkout === this.context &&
! this.shouldUpdateButtonWithFormData()
) {
return true;
}
return false;
);
}
/**
* Updates ApplePay paymentRequest with form data.
* @param paymentRequest
* Updates Apple Pay paymentRequest with form data.
*
* @param {Object} paymentRequest Object to extend with form data.
*/
updateRequestDataWithForm( paymentRequest ) {
if ( ! this.shouldUpdateButtonWithFormData() ) {
@ -358,8 +634,9 @@ class ApplepayButton {
);
// Add custom data.
// "applicationData" is originating a "PayPalApplePayError: An internal server error has occurred" on paypal.Applepay().confirmOrder().
// paymentRequest.applicationData = this.fillApplicationData(this.formData);
// "applicationData" is originating a "PayPalApplePayError: An internal server error has
// occurred" on paypal.Applepay().confirmOrder(). paymentRequest.applicationData =
// this.fillApplicationData(this.formData);
if ( ! this.shouldRequireShippingInButton() ) {
return;
@ -425,7 +702,8 @@ class ApplepayButton {
'email',
'phone',
],
requiredBillingContactFields: [ 'postalAddress' ], // ApplePay does not implement billing email and phone fields.
requiredBillingContactFields: [ 'postalAddress' ], // ApplePay does not implement billing
// email and phone fields.
};
if ( ! this.shouldRequireShippingInButton() ) {
@ -453,14 +731,11 @@ class ApplepayButton {
}
refreshContextData() {
switch ( this.context ) {
case 'product':
// Refresh product data that makes the price change.
this.productQuantity =
document.querySelector( 'input.qty' )?.value;
this.products = this.contextHandler.products();
this.log( 'Products updated', this.products );
break;
if ( CONTEXT.Product === this.context ) {
// Refresh product data that makes the price change.
this.productQuantity = document.querySelector( 'input.qty' )?.value;
this.products = this.contextHandler.products();
this.log( 'Products updated', this.products );
}
}
@ -468,8 +743,36 @@ class ApplepayButton {
// Payment process
//------------------------
/**
* Make ajax call to change the verification-status of the current domain.
*
* @param {boolean} isValid
*/
adminValidation( isValid ) {
// eslint-disable-next-line no-unused-vars
const ignored = fetch( this.buttonConfig.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams( {
action: 'ppcp_validate',
'woocommerce-process-checkout-nonce': this.nonce,
validation: isValid,
} ).toString(),
} );
}
/**
* Returns an event handler that Apple Pay calls when displaying the payment sheet.
*
* @see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778021-onvalidatemerchant
*
* @param {Object} session The ApplePaySession object.
*
* @return {(function(*): void)|*} Callback that runs after the merchant validation
*/
onValidateMerchant( session ) {
this.log( 'onvalidatemerchant', this.buttonConfig.ajax_url );
return ( applePayValidateMerchantEvent ) => {
this.log( 'onvalidatemerchant call' );
@ -479,34 +782,15 @@ class ApplepayButton {
validationUrl: applePayValidateMerchantEvent.validationURL,
} )
.then( ( validateResult ) => {
this.log( 'onvalidatemerchant ok' );
session.completeMerchantValidation(
validateResult.merchantSession
);
//call backend to update validation to true
jQuery.ajax( {
url: this.buttonConfig.ajax_url,
type: 'POST',
data: {
action: 'ppcp_validate',
validation: true,
'woocommerce-process-checkout-nonce': this.nonce,
},
} );
this.adminValidation( true );
} )
.catch( ( validateError ) => {
this.log( 'onvalidatemerchant error', validateError );
console.error( validateError );
//call backend to update validation to false
jQuery.ajax( {
url: this.buttonConfig.ajax_url,
type: 'POST',
data: {
action: 'ppcp_validate',
validation: false,
'woocommerce-process-checkout-nonce': this.nonce,
},
} );
this.adminValidation( false );
this.log( 'onvalidatemerchant session abort' );
session.abort();
} );
@ -515,14 +799,14 @@ class ApplepayButton {
onShippingMethodSelected( session ) {
this.log( 'onshippingmethodselected', this.buttonConfig.ajax_url );
const ajax_url = this.buttonConfig.ajax_url;
const ajaxUrl = this.buttonConfig.ajax_url;
return ( event ) => {
this.log( 'onshippingmethodselected call' );
const data = this.getShippingMethodData( event );
jQuery.ajax( {
url: ajax_url,
url: ajaxUrl,
method: 'POST',
data,
success: (
@ -537,7 +821,8 @@ class ApplepayButton {
}
this.selectedShippingMethod = event.shippingMethod;
// Sort the response shipping methods, so that the selected shipping method is the first one.
// Sort the response shipping methods, so that the selected shipping method is
// the first one.
response.newShippingMethods =
response.newShippingMethods.sort( ( a, b ) => {
if (
@ -565,7 +850,7 @@ class ApplepayButton {
onShippingContactSelected( session ) {
this.log( 'onshippingcontactselected', this.buttonConfig.ajax_url );
const ajax_url = this.buttonConfig.ajax_url;
const ajaxUrl = this.buttonConfig.ajax_url;
return ( event ) => {
this.log( 'onshippingcontactselected call' );
@ -573,7 +858,7 @@ class ApplepayButton {
const data = this.getShippingContactData( event );
jQuery.ajax( {
url: ajax_url,
url: ajaxUrl,
method: 'POST',
data,
success: (
@ -603,15 +888,15 @@ class ApplepayButton {
}
getShippingContactData( event ) {
const product_id = this.buttonConfig.product.id;
const productId = this.buttonConfig.product.id;
this.refreshContextData();
switch ( this.context ) {
case 'product':
case CONTEXT.Product:
return {
action: 'ppcp_update_shipping_contact',
product_id,
product_id: productId,
products: JSON.stringify( this.products ),
caller_page: 'productDetail',
product_quantity: this.productQuantity,
@ -619,11 +904,12 @@ class ApplepayButton {
need_shipping: this.shouldRequireShippingInButton(),
'woocommerce-process-checkout-nonce': this.nonce,
};
case 'cart':
case 'checkout':
case 'cart-block':
case 'checkout-block':
case 'mini-cart':
case CONTEXT.Cart:
case CONTEXT.Checkout:
case CONTEXT.BlockCart:
case CONTEXT.BlockCheckout:
case CONTEXT.MiniCart:
return {
action: 'ppcp_update_shipping_contact',
simplified_contact: event.shippingContact,
@ -635,12 +921,12 @@ class ApplepayButton {
}
getShippingMethodData( event ) {
const product_id = this.buttonConfig.product.id;
const productId = this.buttonConfig.product.id;
this.refreshContextData();
switch ( this.context ) {
case 'product':
case CONTEXT.Product:
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
@ -650,17 +936,18 @@ class ApplepayButton {
? this.updatedContactInfo
: this.initialPaymentRequest?.shippingContact ??
this.initialPaymentRequest?.billingContact,
product_id,
product_id: productId,
products: JSON.stringify( this.products ),
caller_page: 'productDetail',
product_quantity: this.productQuantity,
'woocommerce-process-checkout-nonce': this.nonce,
};
case 'cart':
case 'checkout':
case 'cart-block':
case 'checkout-block':
case 'mini-cart':
case CONTEXT.Cart:
case CONTEXT.Checkout:
case CONTEXT.BlockCart:
case CONTEXT.BlockCheckout:
case CONTEXT.MiniCart:
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
@ -681,9 +968,6 @@ class ApplepayButton {
return async ( event ) => {
this.log( 'onpaymentauthorized call' );
function form() {
return document.querySelector( 'form.cart' );
}
const processInWooAndCapture = async ( data ) => {
return new Promise( ( resolve, reject ) => {
try {
@ -698,7 +982,7 @@ class ApplepayButton {
( this.initialPaymentRequest.shippingMethods ||
[] )[ 0 ];
const request_data = {
const requestData = {
action: 'ppcp_create_order',
caller_page: this.context,
product_id: this.buttonConfig.product.id ?? null,
@ -723,7 +1007,7 @@ class ApplepayButton {
jQuery.ajax( {
url: this.buttonConfig.ajax_url,
method: 'POST',
data: request_data,
data: requestData,
complete: ( jqXHR, textStatus ) => {
this.log( 'onpaymentauthorized complete' );
},
@ -785,7 +1069,8 @@ class ApplepayButton {
if (
this.shouldCompletePaymentWithContextHandler()
) {
// No shipping, expect immediate capture, ex: PayNow, Checkout with form data.
// No shipping, expect immediate capture, ex: PayNow, Checkout with
// form data.
let approveFailed = false;
await this.contextHandler.approveOrder(
@ -960,4 +1245,4 @@ class ApplepayButton {
}
}
export default ApplepayButton;
export default ApplePayButton;

View file

@ -1,7 +1,9 @@
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import ApplepayButton from './ApplepayButton';
/* global paypal */
class ApplepayManager {
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import ApplePayButton from './ApplepayButton';
class ApplePayManager {
constructor( buttonConfig, ppcpConfig ) {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
@ -9,7 +11,7 @@ class ApplepayManager {
this.buttons = [];
buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => {
const button = new ApplepayButton(
const button = new ApplePayButton(
bootstrap.context,
bootstrap.handler,
buttonConfig,
@ -40,8 +42,7 @@ class ApplepayManager {
}
/**
* Gets ApplePay configuration of the PayPal merchant.
* @return {Promise<null>}
* Gets Apple Pay configuration of the PayPal merchant.
*/
async config() {
this.ApplePayConfig = await paypal.Applepay().config();
@ -49,4 +50,4 @@ class ApplepayManager {
}
}
export default ApplepayManager;
export default ApplePayManager;

View file

@ -1,6 +1,8 @@
import ApplepayButton from './ApplepayButton';
/* global paypal */
class ApplepayManagerBlockEditor {
import ApplePayButton from './ApplepayButton';
class ApplePayManagerBlockEditor {
constructor( buttonConfig, ppcpConfig ) {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
@ -17,7 +19,7 @@ class ApplepayManagerBlockEditor {
try {
this.applePayConfig = await paypal.Applepay().config();
const button = new ApplepayButton(
const button = new ApplePayButton(
this.ppcpConfig.context,
null,
this.buttonConfig,
@ -31,4 +33,4 @@ class ApplepayManagerBlockEditor {
}
}
export default ApplepayManagerBlockEditor;
export default ApplePayManagerBlockEditor;

View file

@ -1,10 +1,6 @@
import BaseHandler from './BaseHandler';
class PreviewHandler extends BaseHandler {
constructor( buttonConfig, ppcpConfig, externalHandler ) {
super( buttonConfig, ppcpConfig, externalHandler );
}
transactionInfo() {
// We need to return something as ApplePay button initialization expects valid data.
return {
@ -19,7 +15,7 @@ class PreviewHandler extends BaseHandler {
throw new Error( 'Create order fail. This is just a preview.' );
}
approveOrder( data, actions ) {
approveOrder() {
throw new Error( 'Approve order fail. This is just a preview.' );
}

View file

@ -1,4 +1,4 @@
export const buttonID = 'applepay-container';
export const buttonID = 'ppc-button-applepay-container';
export const endpoints = {
validation: '_apple_pay_validation',
createOrderCart: '_apple_pay_create_order_cart',

View file

@ -1,4 +1,4 @@
import ApplepayButton from './ApplepayButton';
import ApplePayButton from './ApplepayButton';
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
@ -86,7 +86,7 @@ class ApplePayPreviewButton extends PreviewButton {
}
createButton( buttonConfig ) {
const button = new ApplepayButton(
const button = new ApplePayButton(
'preview',
null,
buttonConfig,

View file

@ -4,8 +4,8 @@ import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Help
import { cartHasSubscriptionProducts } from '../../../ppcp-blocks/resources/js/Helper/Subscription';
import { loadCustomScript } from '@paypal/paypal-js';
import CheckoutHandler from './Context/CheckoutHandler';
import ApplepayManager from './ApplepayManager';
import ApplepayManagerBlockEditor from './ApplepayManagerBlockEditor';
import ApplePayManager from './ApplepayManager';
import ApplePayManagerBlockEditor from './ApplepayManagerBlockEditor';
const ppcpData = wc.wcSettings.getSetting( 'ppcp-gateway_data' );
const ppcpConfig = ppcpData.scriptData;
@ -24,8 +24,8 @@ const ApplePayComponent = ( props ) => {
const bootstrap = function () {
const ManagerClass = props.isEditing
? ApplepayManagerBlockEditor
: ApplepayManager;
? ApplePayManagerBlockEditor
: ApplePayManager;
const manager = new ManagerClass( buttonConfig, ppcpConfig );
manager.init();
};

View file

@ -1,13 +1,13 @@
import { loadCustomScript } from '@paypal/paypal-js';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
import ApplepayManager from './ApplepayManager';
import ApplePayManager from './ApplepayManager';
import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper';
( function ( { buttonConfig, ppcpConfig, jQuery } ) {
let manager;
const bootstrap = function () {
manager = new ApplepayManager( buttonConfig, ppcpConfig );
manager = new ApplePayManager( buttonConfig, ppcpConfig );
manager.init();
};

View file

@ -299,5 +299,15 @@ return array(
esc_html( $button_text )
);
},
'applepay.wc-gateway' => static function ( ContainerInterface $container ): ApplePayGateway {
return new ApplePayGateway(
$container->get( 'wcgateway.order-processor' ),
$container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'session.handler' ),
$container->get( 'applepay.url' )
);
},
);

View file

@ -0,0 +1,231 @@
<?php
/**
* The Apple Pay Payment Gateway
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Applepay;
use Exception;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\Messages;
/**
* Class ApplePayGateway
*/
class ApplePayGateway extends WC_Payment_Gateway {
use ProcessPaymentTrait;
const ID = 'ppcp-applepay';
/**
* The processor for orders.
*
* @var OrderProcessor
*/
protected $order_processor;
/**
* The function return the PayPal checkout URL for the given order ID.
*
* @var callable(string):string
*/
private $paypal_checkout_url_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* The Session Handler.
*
* @var SessionHandler
*/
protected $session_handler;
/**
* The URL to the module.
*
* @var string
*/
private $module_url;
/**
* ApplePayGateway constructor.
*
* @param OrderProcessor $order_processor The Order Processor.
* @param callable(string):string $paypal_checkout_url_factory The function return the PayPal
* checkout URL for the given order
* ID.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction
* view URL based on order.
* @param SessionHandler $session_handler The Session Handler.
* @param string $module_url The URL to the module.
*/
public function __construct(
OrderProcessor $order_processor,
callable $paypal_checkout_url_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider,
SessionHandler $session_handler,
string $module_url
) {
$this->id = self::ID;
$this->method_title = __( 'Apple Pay (via PayPal) ', 'woocommerce-paypal-payments' );
$this->method_description = __( 'Display Apple Pay as a standalone payment option instead of bundling it with PayPal.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'Apple Pay', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->module_url = $module_url;
$this->icon = esc_url( $this->module_url ) . 'assets/images/applepay.png';
$this->init_form_fields();
$this->init_settings();
$this->order_processor = $order_processor;
$this->paypal_checkout_url_factory = $paypal_checkout_url_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
$this->session_handler = $session_handler;
add_action(
'woocommerce_update_options_payment_gateways_' . $this->id,
array( $this, 'process_admin_options' )
);
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable Apple Pay payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Process payment for a WooCommerce order.
*
* @param int $order_id The WooCommerce order id.
*
* @return array
*/
public function process_payment( $order_id ) : array {
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return $this->handle_payment_failure(
null,
new GatewayGenericException( new Exception( 'WC order was not found.' ) )
);
}
do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order );
try {
try {
$this->order_processor->process( $wc_order );
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );
return $this->handle_payment_success( $wc_order );
} catch ( PayPalOrderMissingException $exc ) {
$order = $this->order_processor->create_order( $wc_order );
return array(
'result' => 'success',
'redirect' => ( $this->paypal_checkout_url_factory )( $order->id() ),
);
}
} catch ( PayPalApiException $error ) {
return $this->handle_payment_failure(
$wc_order,
new Exception(
Messages::generic_payment_error_message() . ' ' . $error->getMessage(),
$error->getCode(),
$error
)
);
} catch ( Exception $error ) {
return $this->handle_payment_failure( $wc_order, $error );
}
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
*
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) : bool {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ) : string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Applepay;
use WC_Payment_Gateway;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton;
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
@ -17,10 +18,9 @@ use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -125,6 +125,48 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
2
);
add_filter(
'woocommerce_payment_gateways',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
static function ( $methods ) use ( $c ): array {
if ( ! is_array( $methods ) ) {
return $methods;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( $settings->has( 'applepay_button_enabled' ) && $settings->get( 'applepay_button_enabled' ) ) {
$applepay_gateway = $c->get( 'applepay.wc-gateway' );
assert( $applepay_gateway instanceof WC_Payment_Gateway );
$methods[] = $applepay_gateway;
}
return $methods;
}
);
add_action(
'woocommerce_review_order_after_submit',
function () {
// Wrapper ID: #ppc-button-ppcp-applepay.
echo '<div id="ppc-button-' . esc_attr( ApplePayGateway::ID ) . '"></div>';
}
);
add_action(
'woocommerce_pay_order_after_submit',
function () {
// Wrapper ID: #ppc-button-ppcp-applepay.
echo '<div id="ppc-button-' . esc_attr( ApplePayGateway::ID ) . '"></div>';
}
);
return true;
}
@ -307,7 +349,7 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
* @param bool $is_sandbox The environment for this merchant.
* @return string
*/
public function validation_string( bool $is_sandbox ) {
public function validation_string( bool $is_sandbox ) : string {
$sandbox_string = $this->sandbox_validation_string();
$live_string = $this->live_validation_string();
return $is_sandbox ? $sandbox_string : $live_string;

View file

@ -20,12 +20,13 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandlerTrait;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
/**
* Class ApplePayButton
*/
class ApplePayButton implements ButtonInterface {
use RequestHandlerTrait;
use RequestHandlerTrait, ContextTrait;
/**
* The settings.
@ -340,7 +341,7 @@ class ApplePayButton implements ButtonInterface {
}
$response = $this->response_templates->apple_formatted_response( $payment_details );
$this->response_templates->response_success( $response );
} catch ( \Exception $e ) {
} catch ( Exception $e ) {
$this->response_templates->response_with_data_errors(
array(
array(
@ -382,7 +383,7 @@ class ApplePayButton implements ButtonInterface {
}
$response = $this->response_templates->apple_formatted_response( $payment_details );
$this->response_templates->response_success( $response );
} catch ( \Exception $e ) {
} catch ( Exception $e ) {
$this->response_templates->response_with_data_errors(
array(
array(
@ -399,7 +400,7 @@ class ApplePayButton implements ButtonInterface {
* On error returns an array of errors to be handled by the script
* On success returns the new order data
*
* @throws \Exception When validation fails.
* @throws Exception When validation fails.
*/
public function create_wc_order(): void {
$applepay_request_data_object = $this->applepay_data_object_http();
@ -420,15 +421,18 @@ class ApplePayButton implements ButtonInterface {
$applepay_request_data_object->order_data( $context );
$this->update_posted_data( $applepay_request_data_object );
if ( $context === 'product' ) {
$cart_item_key = $this->prepare_cart( $applepay_request_data_object );
$cart = WC()->cart;
$address = $applepay_request_data_object->shipping_address();
$this->calculate_totals_single_product(
$cart,
$address,
$applepay_request_data_object->shipping_method()
);
if ( ! $cart_item_key ) {
$this->response_templates->response_with_data_errors(
array(
@ -438,19 +442,16 @@ class ApplePayButton implements ButtonInterface {
),
)
);
return;
}
add_filter(
'woocommerce_payment_successful_result',
function ( array $result ) use ( $cart, $cart_item_key ) : array {
if ( ! is_string( $cart_item_key ) ) {
} else {
add_filter(
'woocommerce_payment_successful_result',
function ( array $result ) use ( $cart, $cart_item_key ) : array {
$this->clear_current_cart( $cart, $cart_item_key );
$this->reload_cart( $cart );
return $result;
}
$this->clear_current_cart( $cart, $cart_item_key );
$this->reload_cart( $cart );
return $result;
}
);
);
}
}
WC()->checkout()->process_checkout();
@ -460,17 +461,20 @@ class ApplePayButton implements ButtonInterface {
/**
* Checks if the nonce in the data object is valid
*
* @return bool|int
* @return bool
*/
protected function is_nonce_valid(): bool {
$nonce = filter_input( INPUT_POST, 'woocommerce-process-checkout-nonce', FILTER_SANITIZE_SPECIAL_CHARS );
if ( ! $nonce ) {
return false;
}
return wp_verify_nonce(
// Return value 1 indicates "valid nonce, generated in past 12 hours".
// Return value 2 also indicated valid nonce, but older than 12 hours.
return 1 === wp_verify_nonce(
$nonce,
'woocommerce-process_checkout'
) === 1;
);
}
/**
@ -511,7 +515,7 @@ class ApplePayButton implements ButtonInterface {
$address,
$applepay_request_data_object->shipping_method()
);
if ( is_string( $cart_item_key ) ) {
if ( $cart_item_key ) {
$this->clear_current_cart( $cart, $cart_item_key );
$this->reload_cart( $cart );
}
@ -819,9 +823,9 @@ class ApplePayButton implements ButtonInterface {
/**
* Removes the old cart, saves it, and creates a new one
*
* @throws Exception If it cannot be added to cart.
* @param ApplePayDataObjectHttp $applepay_request_data_object The request data object.
* @return bool | string The cart item key after adding to the new cart.
* @throws \Exception If it cannot be added to cart.
* @return string The cart item key after adding to the new cart.
*/
public function prepare_cart( ApplePayDataObjectHttp $applepay_request_data_object ): string {
$this->save_old_cart();
@ -838,7 +842,7 @@ class ApplePayButton implements ButtonInterface {
);
$this->cart_products->add_products( array( $product ) );
return $this->cart_products->cart_item_keys()[0];
return $this->cart_products->cart_item_keys()[0] ?? '';
}
/**
@ -949,6 +953,7 @@ class ApplePayButton implements ButtonInterface {
$render_placeholder,
function () {
$this->applepay_button();
$this->hide_gateway_until_eligible();
},
21
);
@ -961,6 +966,7 @@ class ApplePayButton implements ButtonInterface {
$render_placeholder,
function () {
$this->applepay_button();
$this->hide_gateway_until_eligible();
},
21
);
@ -973,7 +979,7 @@ class ApplePayButton implements ButtonInterface {
add_action(
$render_placeholder,
function () {
echo '<span id="applepay-container-minicart" class="ppcp-button-apm ppcp-button-applepay ppcp-button-minicart"></span>';
echo '<span id="ppc-button-applepay-container-minicart" class="ppcp-button-apm ppcp-button-applepay ppcp-button-minicart"></span>';
},
21
);
@ -981,24 +987,29 @@ class ApplePayButton implements ButtonInterface {
return true;
}
/**
* ApplePay button markup
*/
protected function applepay_button(): void {
?>
<div id="applepay-container" class="ppcp-button-apm ppcp-button-applepay">
<div id="ppc-button-applepay-container" class="ppcp-button-apm ppcp-button-applepay">
<?php wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); ?>
</div>
<?php
}
/**
* Checks if the module should load the script.
* Outputs an inline CSS style that hides the Apple Pay gateway (on Classic Checkout).
* The style is removed by `ApplepayButton.js` once the eligibility of the payment method
* is confirmed.
*
* @return bool
* @return void
*/
public function should_load_script(): bool {
return true;
protected function hide_gateway_until_eligible(): void {
?>
<style id="ppcp-hide-apple-pay">.wc_payment_method.payment_method_ppcp-applepay{display:none}</style>
<?php
}
/**

View file

@ -5,12 +5,14 @@
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Applepay\Assets;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WC_Product;
/**
* Class DataToAppleButtonScripts
@ -33,7 +35,7 @@ class DataToAppleButtonScripts {
/**
* DataToAppleButtonScripts constructor.
*
* @param string $sdk_url The URL to the SDK.
* @param string $sdk_url The URL to the SDK.
* @param Settings $settings The settings.
*/
public function __construct( string $sdk_url, Settings $settings ) {
@ -45,57 +47,92 @@ class DataToAppleButtonScripts {
* Sets the appropriate data to send to ApplePay script
* Data differs between product page and cart page
*
* @param bool $is_block Whether the button is in a block or not.
* @return array
* @throws NotFoundException When the setting is not found.
*/
public function apple_pay_script_data( bool $is_block = false ): array {
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'];
$currency_code = get_woocommerce_currency();
$total_label = get_bloginfo( 'name' );
public function apple_pay_script_data() : array {
if ( is_product() ) {
return $this->data_for_product_page(
$shop_country_code,
$currency_code,
$total_label
);
return $this->data_for_product_page();
}
return $this->data_for_cart_page(
$shop_country_code,
$currency_code,
$total_label
);
return $this->data_for_cart_page();
}
/**
* Returns the appropriate admin data to send to ApplePay script
*
* @return array
* @throws NotFoundException When the setting is not found.
*/
public function apple_pay_script_data_for_admin() : array {
return $this->data_for_admin_page();
}
/**
* Returns the full config array for the Apple Pay integration with default values.
*
* @param array $product - Optional. Product details for the payment button.
*
* @return array
*/
private function get_apple_pay_data( array $product = array() ) : array {
// true: Use Apple Pay as distinct gateway.
// false: integrate it with the smart buttons.
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$is_wc_gateway_enabled = isset( $available_gateways[ ApplePayGateway::ID ] );
// use_wc: Use WC checkout data
// use_applepay: Use data provided by Apple Pay.
$checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' )
? $this->settings->get( 'applepay_checkout_data_mode' )
: PropertiesDictionary::BILLING_DATA_MODE_DEFAULT;
// Store country, currency and name.
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'];
$currency_code = get_woocommerce_currency();
$total_label = get_bloginfo( 'name' );
return $this->data_for_admin_page(
$shop_country_code,
$currency_code,
$total_label
// Button layout (label, color, language).
$type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : '';
$color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : '';
$lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : '';
$lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang );
$is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' );
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'is_admin' => false,
'is_enabled' => $is_enabled,
'is_wc_gateway_enabled' => $is_wc_gateway_enabled,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
'button' => array(
'wrapper' => 'ppc-button-applepay-container',
'mini_cart_wrapper' => 'ppc-button-applepay-container-minicart',
'type' => $type,
'color' => $color,
'lang' => $lang,
),
'product' => $product,
'shop' => array(
'countryCode' => $shop_country_code,
'currencyCode' => $currency_code,
'totalLabel' => $total_label,
),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ),
);
}
/**
* Check if the product needs shipping
*
* @param \WC_Product $product The product.
* @param WC_Product $product Product to check.
*
* @return bool
*/
protected function check_if_need_shipping( $product ) {
protected function check_if_need_shipping( WC_Product $product ) : bool {
if (
! wc_shipping_enabled()
|| 0 === wc_get_shipping_method_count(
@ -104,30 +141,20 @@ class DataToAppleButtonScripts {
) {
return false;
}
$needs_shipping = false;
if ( $product->needs_shipping() ) {
$needs_shipping = true;
return true;
}
return $needs_shipping;
return false;
}
/**
* Prepares the data for the product page.
*
* @param string $shop_country_code The shop country code.
* @param string $currency_code The currency code.
* @param string $total_label The label for the total amount.
*
* @return array
* @throws NotFoundException When the setting is not found.
*/
protected function data_for_product_page(
$shop_country_code,
$currency_code,
$total_label
) {
protected function data_for_product_page() : array {
$product = wc_get_product( get_the_id() );
if ( ! $product ) {
return array();
@ -136,146 +163,59 @@ class DataToAppleButtonScripts {
if ( $product->get_type() === 'variable' || $product->get_type() === 'variable-subscription' ) {
$is_variation = true;
}
$product_need_shipping = $this->check_if_need_shipping( $product );
$product_id = get_the_id();
$product_price = $product->get_price();
$product_stock = $product->get_stock_status();
$type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : '';
$color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : '';
$lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : '';
$checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT;
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',
'type' => $type,
'color' => $color,
'lang' => $lang,
),
'product' => array(
return $this->get_apple_pay_data(
array(
'needShipping' => $product_need_shipping,
'id' => $product_id,
'price' => $product_price,
'isVariation' => $is_variation,
'stock' => $product_stock,
),
'shop' => array(
'countryCode' => $shop_country_code,
'currencyCode' => $currency_code,
'totalLabel' => $total_label,
),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ),
)
);
}
/**
* Prepares the data for the cart page.
*
* @param string $shop_country_code The shop country code.
* @param string $currency_code The currency code.
* @param string $total_label The label for the total amount.
*
* @return array
*/
protected function data_for_cart_page(
$shop_country_code,
$currency_code,
$total_label
) {
protected function data_for_cart_page() : array {
$cart = WC()->cart;
if ( ! $cart ) {
return array();
}
$type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : '';
$color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : '';
$lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : '';
$lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang );
$checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT;
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'is_admin' => false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',
'type' => $type,
'color' => $color,
'lang' => $lang,
),
'product' => array(
return $this->get_apple_pay_data(
array(
'needShipping' => $cart->needs_shipping(),
'subtotal' => $cart->get_subtotal(),
),
'shop' => array(
'countryCode' => $shop_country_code,
'currencyCode' => $currency_code,
'totalLabel' => $total_label,
),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ),
)
);
}
/**
* Prepares the data for the cart page.
* Consider refactoring this method along with data_for_cart_page() and data_for_product_page() methods.
*
* @param string $shop_country_code The shop country code.
* @param string $currency_code The currency code.
* @param string $total_label The label for the total amount.
* Consider refactoring this method along with data_for_cart_page() and data_for_product_page()
* methods.
*
* @return array
*/
protected function data_for_admin_page(
$shop_country_code,
$currency_code,
$total_label
) {
$type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : '';
$color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : '';
$lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : '';
$lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang );
$checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT;
$is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' );
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'is_admin' => true,
'is_enabled' => $is_enabled,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',
'type' => $type,
'color' => $color,
'lang' => $lang,
),
'product' => array(
protected function data_for_admin_page() : array {
$data = $this->get_apple_pay_data(
array(
'needShipping' => false,
'subtotal' => 0,
),
'shop' => array(
'countryCode' => $shop_country_code,
'currencyCode' => $currency_code,
'totalLabel' => $total_label,
),
'ajax_url' => admin_url( 'admin-ajax.php' ),
)
);
$data['is_admin'] = true;
return $data;
}
}

View file

@ -61,7 +61,7 @@ return array(
'title' => __( 'Require final confirmation on checkout', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => $label,
'default' => false,
'default' => true,
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(),
'gateway' => 'paypal',

View file

@ -10,7 +10,7 @@
"Edge >= 14"
],
"dependencies": {
"@paypal/react-paypal-js": "^8.3.0",
"@paypal/react-paypal-js": "^8.5.0",
"core-js": "^3.25.0",
"react": "^17.0.0",
"react-dom": "^17.0.0"

View file

@ -7,7 +7,12 @@ import {
} from '@paypal/react-paypal-js';
import { CheckoutHandler } from './checkout-handler';
import { createOrder, onApprove } from '../card-fields-config';
import {
createOrder,
onApprove,
createVaultSetupToken,
onApproveSavePayment,
} from '../card-fields-config';
import { cartHasSubscriptionProducts } from '../Helper/Subscription';
export function CardFields( {
@ -70,8 +75,21 @@ export function CardFields( {
} }
>
<PayPalCardFieldsProvider
createOrder={ createOrder }
onApprove={ onApprove }
createVaultSetupToken={
config.scriptData.is_free_trial_cart
? createVaultSetupToken
: undefined
}
createOrder={
config.scriptData.is_free_trial_cart
? undefined
: createOrder
}
onApprove={
config.scriptData.is_free_trial_cart
? onApproveSavePayment
: onApprove
}
onError={ ( err ) => {
console.error( err );
} }

View file

@ -44,3 +44,61 @@ export async function onApprove( data ) {
console.error( err );
} );
}
export async function createVaultSetupToken() {
const config = wc.wcSettings.getSetting( 'ppcp-credit-card-gateway_data' );
return fetch( config.scriptData.ajax.create_setup_token.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: config.scriptData.ajax.create_setup_token.nonce,
payment_method: 'ppcp-credit-card-gateway',
} ),
} )
.then( ( response ) => response.json() )
.then( ( result ) => {
console.log( result );
return result.data.id;
} )
.catch( ( err ) => {
console.error( err );
} );
}
export async function onApproveSavePayment( { vaultSetupToken } ) {
const config = wc.wcSettings.getSetting( 'ppcp-credit-card-gateway_data' );
let endpoint =
config.scriptData.ajax.create_payment_token_for_guest.endpoint;
let bodyContent = {
nonce: config.scriptData.ajax.create_payment_token_for_guest.nonce,
vault_setup_token: vaultSetupToken,
};
if ( config.scriptData.user.is_logged_in ) {
endpoint = config.scriptData.ajax.create_payment_token.endpoint;
bodyContent = {
nonce: config.scriptData.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
is_free_trial_cart: config.scriptData.is_free_trial_cart,
};
}
const response = await fetch( endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( bodyContent ),
} );
const result = await response.json();
if ( result.success !== true ) {
console.error( result );
}
}

View file

@ -227,7 +227,7 @@ const PayPalComponent = ( {
throw new Error( config.scriptData.labels.error.generic );
}
if ( ! shouldHandleShippingInPayPal() ) {
if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
@ -318,7 +318,7 @@ const PayPalComponent = ( {
throw new Error( config.scriptData.labels.error.generic );
}
if ( ! shouldHandleShippingInPayPal() ) {
if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
@ -364,16 +364,20 @@ const PayPalComponent = ( {
};
const shouldHandleShippingInPayPal = () => {
if ( config.finalReviewEnabled ) {
return false;
}
return (
window.ppcpFundingSource !== 'venmo' ||
! config.scriptData.vaultingEnabled
);
return shouldskipFinalConfirmation() && config.needShipping
};
const shouldskipFinalConfirmation = () => {
if ( config.finalReviewEnabled ) {
return false;
}
return (
window.ppcpFundingSource !== 'venmo' ||
! config.scriptData.vaultingEnabled
);
};
let handleShippingOptionsChange = null;
let handleShippingAddressChange = null;
let handleSubscriptionShippingOptionsChange = null;
@ -544,7 +548,7 @@ const PayPalComponent = ( {
if ( config.scriptData.continuation ) {
return true;
}
if ( shouldHandleShippingInPayPal() ) {
if ( shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
}
return true;

View file

@ -97,15 +97,6 @@ class UpdateShippingEndpoint implements EndpointInterface {
$pu = $this->purchase_unit_factory->from_wc_cart( null, true );
$pu_data = $pu->to_array();
if ( ! isset( $pu_data['shipping']['options'] ) ) {
wp_send_json_error(
array(
'message' => 'No shipping methods.',
)
);
return false;
}
// TODO: maybe should patch only if methods changed.
// But it seems a bit difficult to detect,
// e.g. ->order($id) may not have Shipping because we drop it when address or name are missing.

View file

@ -254,6 +254,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
),
),
'scriptData' => $script_data,
'needShipping' => WC()->cart->needs_shipping(),
);
}

View file

@ -1005,19 +1005,19 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@paypal/paypal-js@^8.0.5":
version "8.0.5"
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.0.5.tgz#77bc461b4d1e5a2c6f081269e3ef0b2e3331a68c"
integrity sha512-yQNV7rOILeaVCNU4aVDRPqEnbIlzfxgQfFsxzsBuZW1ouqRD/4kYBWJDzczCiscSr2xOeA/Pkm7e3a9fRfnuMQ==
"@paypal/paypal-js@^8.1.0":
version "8.1.0"
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.1.0.tgz#4e7d10e0a0b4164985029cfdac748e5694d117e9"
integrity sha512-f64bom5xYwmxyeKPJUFS/XpM0tXojQEgjRIADPqe1R9WmK+PFqL4SEkT85cGU0ZXLVx4EGbjwREHhqEOR+OstA==
dependencies:
promise-polyfill "^8.3.0"
"@paypal/react-paypal-js@^8.3.0":
version "8.3.0"
resolved "https://registry.yarnpkg.com/@paypal/react-paypal-js/-/react-paypal-js-8.3.0.tgz#a103080b752766b8ff59b8620887abf802e1a01b"
integrity sha512-SX17d2h1CMNFGI+wtjb329AEDaBR8Ziy2LCV076eDcY1Q0MFKRkfQ/v0HOAvZtk3sJoydRmYez2pq47BRblwqQ==
"@paypal/react-paypal-js@^8.5.0":
version "8.5.0"
resolved "https://registry.yarnpkg.com/@paypal/react-paypal-js/-/react-paypal-js-8.5.0.tgz#cf17483202c8fa7a33dae86798d50a102705f182"
integrity sha512-YIAyLw4OiUoHHoUgXvibrBDdluzqnqVMGsJXyBcoOzlWHQIe5zhh8dgYezNNRXjXwy6t22YmPljjw7lr+eD9cw==
dependencies:
"@paypal/paypal-js" "^8.0.5"
"@paypal/paypal-js" "^8.1.0"
"@paypal/sdk-constants" "^1.0.122"
"@paypal/sdk-constants@^1.0.122":

View file

@ -7,6 +7,7 @@ import Renderer from './modules/Renderer/Renderer';
import ErrorHandler from './modules/ErrorHandler';
import HostedFieldsRenderer from './modules/Renderer/HostedFieldsRenderer';
import CardFieldsRenderer from './modules/Renderer/CardFieldsRenderer';
import CardFieldsFreeTrialRenderer from './modules/Renderer/CardFieldsFreeTrialRenderer';
import MessageRenderer from './modules/Renderer/MessageRenderer';
import Spinner from './modules/Helper/Spinner';
import {
@ -215,12 +216,23 @@ const bootstrap = () => {
spinner
);
if ( typeof paypal.CardFields !== 'undefined' ) {
creditCardRenderer = new CardFieldsRenderer(
PayPalCommerceGateway,
errorHandler,
spinner,
onCardFieldsBeforeSubmit
);
if (
PayPalCommerceGateway.is_free_trial_cart &&
PayPalCommerceGateway.user?.has_wc_card_payment_tokens !== true
) {
creditCardRenderer = new CardFieldsFreeTrialRenderer(
PayPalCommerceGateway,
errorHandler,
spinner
);
} else {
creditCardRenderer = new CardFieldsRenderer(
PayPalCommerceGateway,
errorHandler,
spinner,
onCardFieldsBeforeSubmit
);
}
}
const renderer = new Renderer(

View file

@ -173,61 +173,6 @@ class CheckoutActionHandler {
},
};
}
addPaymentMethodConfiguration() {
return {
createVaultSetupToken: async () => {
const response = await fetch(
this.config.ajax.create_setup_token.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: this.config.ajax.create_setup_token.nonce,
} ),
}
);
const result = await response.json();
if ( result.data.id ) {
return result.data.id;
}
console.error( result );
},
onApprove: async ( { vaultSetupToken } ) => {
const response = await fetch(
this.config.ajax.create_payment_token_for_guest.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: this.config.ajax
.create_payment_token_for_guest.nonce,
vault_setup_token: vaultSetupToken,
} ),
}
);
const result = await response.json();
if ( result.success === true ) {
document.querySelector( '#place_order' ).click();
return;
}
console.error( result );
},
onError: ( error ) => {
console.error( error );
},
};
}
}
export default CheckoutActionHandler;

View file

@ -1,3 +1,5 @@
/* global PayPalCommerceGateway */
import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler';
import { setVisible, setVisibleByClass } from '../Helper/Hiding';
import {
@ -7,6 +9,11 @@ import {
PaymentMethods,
} from '../Helper/CheckoutMethodState';
import BootstrapHelper from '../Helper/BootstrapHelper';
import { addPaymentMethodConfiguration } from '../../../../../ppcp-save-payment-methods/resources/js/Configuration';
import {
ButtonEvents,
dispatchButtonEvent,
} from '../Helper/PaymentButtonHelpers';
class CheckoutBootstap {
constructor( gateway, renderer, spinner, errorHandler ) {
@ -68,6 +75,7 @@ class CheckoutBootstap {
jQuery( document.body ).on(
'updated_checkout payment_method_selected',
() => {
this.invalidatePaymentMethods();
this.updateUi();
}
);
@ -160,7 +168,7 @@ class CheckoutBootstap {
PayPalCommerceGateway.vault_v3_enabled
) {
this.renderer.render(
actionHandler.addPaymentMethodConfiguration(),
addPaymentMethodConfiguration( PayPalCommerceGateway ),
{},
actionHandler.configuration()
);
@ -174,6 +182,14 @@ class CheckoutBootstap {
);
}
invalidatePaymentMethods() {
/**
* Custom JS event to notify other modules that the payment button on the checkout page
* has become irrelevant or invalid.
*/
dispatchButtonEvent( { event: ButtonEvents.INVALIDATE } );
}
updateUi() {
const currentPaymentMethod = getCurrentPaymentMethod();
const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL;
@ -183,12 +199,15 @@ class CheckoutBootstap {
);
const isGooglePayMethod =
currentPaymentMethod === PaymentMethods.GOOGLEPAY;
const isApplePayMethod =
currentPaymentMethod === PaymentMethods.APPLEPAY;
const isSavedCard = isCard && isSavedCardSelected();
const isNotOurGateway =
! isPaypal &&
! isCard &&
! isSeparateButtonGateway &&
! isGooglePayMethod;
! isGooglePayMethod &&
! isApplePayMethod;
const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart;
const hasVaultedPaypal =
PayPalCommerceGateway.vaulted_paypal_email !== '';
@ -232,9 +251,20 @@ class CheckoutBootstap {
}
}
setVisible( '#ppc-button-ppcp-googlepay', isGooglePayMethod );
/**
* Custom JS event that is observed by the relevant payment gateway.
*
* Dynamic part of the event name is the payment method ID, for example
* "ppcp-credit-card-gateway" or "ppcp-googlepay"
*/
dispatchButtonEvent( {
event: ButtonEvents.RENDER,
paymentMethod: currentPaymentMethod,
} );
jQuery( document.body ).trigger( 'ppcp_checkout_rendered' );
setVisible( '#ppc-button-ppcp-applepay', isApplePayMethod );
document.body.dispatchEvent( new Event( 'ppcp_checkout_rendered' ) );
}
shouldShowMessages() {

View file

@ -26,8 +26,11 @@ export function setupButtonEvents( refresh ) {
document.addEventListener( REFRESH_BUTTON_EVENT, debouncedRefresh );
// Listen for cart and checkout update events.
document.body.addEventListener( 'updated_cart_totals', debouncedRefresh );
document.body.addEventListener( 'updated_checkout', debouncedRefresh );
// 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( () => {

View file

@ -4,6 +4,31 @@ export const PaymentMethods = {
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';

View file

@ -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 );
}

View file

@ -20,10 +20,7 @@ const onApprove = ( context, errorHandler ) => {
} )
.then( ( data ) => {
if ( ! data.success ) {
errorHandler.genericError();
return actions.restart().catch( ( err ) => {
errorHandler.genericError();
} );
location.href = context.config.redirect;
}
const orderReceivedUrl = data.data?.order_received_url;

View file

@ -0,0 +1,88 @@
import { show } from '../Helper/Hiding';
import { renderFields } from '../../../../../ppcp-card-fields/resources/js/Render';
import {
addPaymentMethodConfiguration,
cardFieldsConfiguration,
} from '../../../../../ppcp-save-payment-methods/resources/js/Configuration';
class CardFieldsFreeTrialRenderer {
constructor( defaultConfig, errorHandler, spinner ) {
this.defaultConfig = defaultConfig;
this.errorHandler = errorHandler;
this.spinner = spinner;
}
render( wrapper, contextConfig ) {
if (
( this.defaultConfig.context !== 'checkout' &&
this.defaultConfig.context !== 'pay-now' ) ||
wrapper === null ||
document.querySelector( wrapper ) === null
) {
return;
}
const buttonSelector = wrapper + ' button';
const gateWayBox = document.querySelector(
'.payment_box.payment_method_ppcp-credit-card-gateway'
);
if ( ! gateWayBox ) {
return;
}
const oldDisplayStyle = gateWayBox.style.display;
gateWayBox.style.display = 'block';
const hideDccGateway = document.querySelector( '#ppcp-hide-dcc' );
if ( hideDccGateway ) {
hideDccGateway.parentNode.removeChild( hideDccGateway );
}
this.errorHandler.clear();
let cardFields = paypal.CardFields(
addPaymentMethodConfiguration( this.defaultConfig )
);
if ( this.defaultConfig.user.is_logged ) {
cardFields = paypal.CardFields(
cardFieldsConfiguration( this.defaultConfig, this.errorHandler )
);
}
if ( cardFields.isEligible() ) {
renderFields( cardFields );
}
gateWayBox.style.display = oldDisplayStyle;
show( buttonSelector );
if ( this.defaultConfig.cart_contains_subscription ) {
const saveToAccount = document.querySelector(
'#wc-ppcp-credit-card-gateway-new-payment-method'
);
if ( saveToAccount ) {
saveToAccount.checked = true;
saveToAccount.disabled = true;
}
}
document
.querySelector( buttonSelector )
?.addEventListener( 'click', ( event ) => {
event.preventDefault();
this.spinner.block();
this.errorHandler.clear();
cardFields.submit().catch( ( error ) => {
console.error( error );
} );
} );
}
disableFields() {}
enableFields() {}
}
export default CardFieldsFreeTrialRenderer;

View file

@ -1,5 +1,5 @@
import { show } from '../Helper/Hiding';
import { cardFieldStyles } from '../Helper/CardFieldsHelper';
import { renderFields } from '../../../../../ppcp-card-fields/resources/js/Render';
class CardFieldsRenderer {
constructor(
@ -45,7 +45,7 @@ class CardFieldsRenderer {
hideDccGateway.parentNode.removeChild( hideDccGateway );
}
const cardField = paypal.CardFields( {
const cardFields = paypal.CardFields( {
createOrder: contextConfig.createOrder,
onApprove( data ) {
return contextConfig.onApprove( data );
@ -56,79 +56,8 @@ class CardFieldsRenderer {
},
} );
if ( cardField.isEligible() ) {
const nameField = document.getElementById(
'ppcp-credit-card-gateway-card-name'
);
if ( nameField ) {
const styles = cardFieldStyles( nameField );
const fieldOptions = {
style: { input: styles },
};
if ( nameField.getAttribute( 'placeholder' ) ) {
fieldOptions.placeholder =
nameField.getAttribute( 'placeholder' );
}
cardField
.NameField( fieldOptions )
.render( nameField.parentNode );
nameField.remove();
}
const numberField = document.getElementById(
'ppcp-credit-card-gateway-card-number'
);
if ( numberField ) {
const styles = cardFieldStyles( numberField );
const fieldOptions = {
style: { input: styles },
};
if ( numberField.getAttribute( 'placeholder' ) ) {
fieldOptions.placeholder =
numberField.getAttribute( 'placeholder' );
}
cardField
.NumberField( fieldOptions )
.render( numberField.parentNode );
numberField.remove();
}
const expiryField = document.getElementById(
'ppcp-credit-card-gateway-card-expiry'
);
if ( expiryField ) {
const styles = cardFieldStyles( expiryField );
const fieldOptions = {
style: { input: styles },
};
if ( expiryField.getAttribute( 'placeholder' ) ) {
fieldOptions.placeholder =
expiryField.getAttribute( 'placeholder' );
}
cardField
.ExpiryField( fieldOptions )
.render( expiryField.parentNode );
expiryField.remove();
}
const cvvField = document.getElementById(
'ppcp-credit-card-gateway-card-cvc'
);
if ( cvvField ) {
const styles = cardFieldStyles( cvvField );
const fieldOptions = {
style: { input: styles },
};
if ( cvvField.getAttribute( 'placeholder' ) ) {
fieldOptions.placeholder =
cvvField.getAttribute( 'placeholder' );
}
cardField
.CVVField( fieldOptions )
.render( cvvField.parentNode );
cvvField.remove();
}
if ( cardFields.isEligible() ) {
renderFields( cardFields );
document.dispatchEvent( new CustomEvent( 'hosted_fields_loaded' ) );
}
@ -169,7 +98,7 @@ class CardFieldsRenderer {
return;
}
cardField.submit().catch( ( error ) => {
cardFields.submit().catch( ( error ) => {
this.spinner.unblock();
console.error( error );
this.errorHandler.message(

View file

@ -0,0 +1,822 @@
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
import { apmButtonsInit } from '../Helper/ApmButtons';
import {
getCurrentPaymentMethod,
PaymentContext,
PaymentMethods,
} from '../Helper/CheckoutMethodState';
import {
ButtonEvents,
dispatchButtonEvent,
observeButtonEvent,
} from '../Helper/PaymentButtonHelpers';
/**
* Collection of all available styling options for this button.
*
* @typedef {Object} StylesCollection
* @property {string} Default - Default button styling.
* @property {string} MiniCart - Styles for mini-cart button.
*/
/**
* Collection of all available wrapper IDs that are possible for the button.
*
* @typedef {Object} WrapperCollection
* @property {string} Default - Default button wrapper.
* @property {string} Gateway - Wrapper for separate gateway.
* @property {string} Block - Wrapper for block checkout button.
* @property {string} MiniCart - Wrapper for mini-cart button.
* @property {string} SmartButton - Wrapper for smart button container.
*/
/**
* Adds the provided PaymentButton instance to a global payment-button collection.
*
* This is debugging logic that should not be used on a production site.
*
* @param {string} methodName - Used to group the buttons.
* @param {PaymentButton} button - Appended to the button collection.
*/
const addToDebuggingCollection = ( methodName, button ) => {
window.ppcpPaymentButtonList = window.ppcpPaymentButtonList || {};
const collection = window.ppcpPaymentButtonList;
collection[ methodName ] = collection[ methodName ] || [];
collection[ methodName ].push( button );
};
/**
* Provides a context-independent instance Map for `PaymentButton` components.
*
* This function addresses a potential issue in multi-context environments, such as pages using
* Block-components. In these scenarios, multiple React execution contexts can lead to duplicate
* `PaymentButton` instances. To prevent this, we store instances in a `Map` that is bound to the
* document's `body` (the rendering context) rather than to individual React components
* (execution contexts).
*
* The `Map` is created as a non-enumerable, non-writable, and non-configurable property of
* `document.body` to ensure its integrity and prevent accidental modifications.
*
* @return {Map<any, any>} A Map containing all `PaymentButton` instances for the current page.
*/
const getInstances = () => {
const collectionKey = '__ppcpPBInstances';
if ( ! document.body[ collectionKey ] ) {
Object.defineProperty( document.body, collectionKey, {
value: new Map(),
enumerable: false,
writable: false,
configurable: false,
} );
}
return document.body[ collectionKey ];
};
/**
* Base class for APM payment buttons, like GooglePay and ApplePay.
*
* This class is not intended for the PayPal button.
*/
export default class PaymentButton {
/**
* Defines the implemented payment method.
*
* Used to identify and address the button internally.
* Overwrite this in the derived class.
*
* @type {string}
*/
static methodId = 'generic';
/**
* CSS class that is added to the payment button wrapper.
*
* Overwrite this in the derived class.
*
* @type {string}
*/
static cssClass = '';
/**
* @type {ConsoleLogger}
*/
#logger;
/**
* Whether the payment button is initialized.
*
* @type {boolean}
*/
#isInitialized = false;
/**
* The button's context.
*
* @type {string}
*/
#context;
/**
* Object containing the IDs of all possible wrapper elements that might contain this
* button; only one wrapper is relevant, depending on the value of the context.
*
* @type {Object}
*/
#wrappers;
/**
* @type {StylesCollection}
*/
#styles;
/**
* Keeps track of CSS classes that were added to the wrapper element.
* We use this list to remove CSS classes that we've added, e.g. to change shape from
* pill to rect in the preview.
*
* @type {string[]}
*/
#appliedClasses = [];
/**
* APM relevant configuration; e.g., configuration of the GooglePay button.
*/
#buttonConfig;
/**
* Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
*/
#ppcpConfig;
/**
* A variation of a context bootstrap handler.
*/
#externalHandler;
/**
* A variation of a context handler object, like CheckoutHandler.
* This handler provides a standardized interface for certain standardized checks and actions.
*/
#contextHandler;
/**
* Whether the current browser/website support the payment method.
*
* @type {boolean}
*/
#isEligible = false;
/**
* Whether this button is visible. Modified by `show()` and `hide()`
*
* @type {boolean}
*/
#isVisible = true;
/**
* The currently visible payment button.
*
* @see {PaymentButton.insertButton}
* @type {HTMLElement|null}
*/
#button = null;
/**
* Factory method to create a new PaymentButton while limiting a single instance per context.
*
* @param {string} context - Button context name.
* @param {unknown} externalHandler - Handler object.
* @param {Object} buttonConfig - Payment button specific configuration.
* @param {Object} ppcpConfig - Plugin wide configuration object.
* @param {unknown} contextHandler - Handler object.
* @return {PaymentButton} The button instance.
*/
static createButton(
context,
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
) {
const buttonInstances = getInstances();
const instanceKey = `${ this.methodId }.${ context }`;
if ( ! buttonInstances.has( instanceKey ) ) {
const button = new this(
context,
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
);
buttonInstances.set( instanceKey, button );
}
return buttonInstances.get( instanceKey );
}
/**
* Returns a list with all wrapper IDs for the implemented payment method, categorized by
* context.
*
* @abstract
* @param {Object} buttonConfig - Payment method specific configuration.
* @param {Object} ppcpConfig - Global plugin configuration.
* @return {{MiniCart, Gateway, Block, SmartButton, Default}} The wrapper ID collection.
*/
// eslint-disable-next-line no-unused-vars
static getWrappers( buttonConfig, ppcpConfig ) {
throw new Error( 'Must be implemented in the child class' );
}
/**
* Returns a list of all button styles for the implemented payment method, categorized by
* context.
*
* @abstract
* @param {Object} buttonConfig - Payment method specific configuration.
* @param {Object} ppcpConfig - Global plugin configuration.
* @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
*/
// eslint-disable-next-line no-unused-vars
static getStyles( buttonConfig, ppcpConfig ) {
throw new Error( 'Must be implemented in the child class' );
}
/**
* Initialize the payment button instance.
*
* Do not create new button instances directly; use the `createButton` method instead
* to avoid multiple button instances handling the same context.
*
* @private
* @param {string} context - Button context name.
* @param {Object} externalHandler - Handler object.
* @param {Object} buttonConfig - Payment button specific configuration.
* @param {Object} ppcpConfig - Plugin wide configuration object.
* @param {Object} contextHandler - Handler object.
*/
constructor(
context,
externalHandler = null,
buttonConfig = {},
ppcpConfig = {},
contextHandler = null
) {
if ( this.methodId === PaymentButton.methodId ) {
throw new Error( 'Cannot initialize the PaymentButton base class' );
}
if ( ! buttonConfig ) {
buttonConfig = {};
}
const isDebugging = !! buttonConfig?.is_debug;
const methodName = this.methodId.replace( /^ppcp?-/, '' );
this.#context = context;
this.#buttonConfig = buttonConfig;
this.#ppcpConfig = ppcpConfig;
this.#externalHandler = externalHandler;
this.#contextHandler = contextHandler;
this.#logger = new ConsoleLogger( methodName, context );
if ( isDebugging ) {
this.#logger.enabled = true;
addToDebuggingCollection( methodName, this );
}
this.#wrappers = this.constructor.getWrappers(
this.#buttonConfig,
this.#ppcpConfig
);
this.applyButtonStyles( this.#buttonConfig );
apmButtonsInit( this.#ppcpConfig );
this.initEventListeners();
}
/**
* Internal ID of the payment gateway.
*
* @readonly
* @return {string} The internal gateway ID, defined in the derived class.
*/
get methodId() {
return this.constructor.methodId;
}
/**
* CSS class that is added to the button wrapper.
*
* @readonly
* @return {string} CSS class, defined in the derived class.
*/
get cssClass() {
return this.constructor.cssClass;
}
/**
* Whether the payment button was fully initialized.
*
* @readonly
* @return {boolean} True indicates, that the button was fully initialized.
*/
get isInitialized() {
return this.#isInitialized;
}
/**
* The button's context.
*
* TODO: Convert the string to a context-object (primitive obsession smell)
*
* @readonly
* @return {string} The button context.
*/
get context() {
return this.#context;
}
/**
* Configuration, specific for the implemented payment button.
*
* @return {Object} Configuration object.
*/
get buttonConfig() {
return this.#buttonConfig;
}
/**
* Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
*
* @return {Object} Configuration object.
*/
get ppcpConfig() {
return this.#ppcpConfig;
}
/**
* @return {Object} The bootstrap handler instance, or an empty object.
*/
get externalHandler() {
return this.#externalHandler || {};
}
/**
* Access the button's context handler.
* When no context handler was provided (like for a preview button), an empty object is
* returned.
*
* @return {Object} The context handler instance, or an empty object.
*/
get contextHandler() {
return this.#contextHandler || {};
}
/**
* Whether customers need to provide shipping details during payment.
*
* Can be extended by child classes to take method specific configuration into account.
*
* @return {boolean} True means, shipping fields are displayed and must be filled.
*/
get requiresShipping() {
// Default check: Is shipping enabled in WooCommerce?
return (
'function' === typeof this.contextHandler.shippingAllowed &&
this.contextHandler.shippingAllowed()
);
}
/**
* Button wrapper details.
*
* @readonly
* @return {WrapperCollection} Wrapper IDs.
*/
get wrappers() {
return this.#wrappers;
}
/**
* Returns the context-relevant button style object.
*
* @readonly
* @return {string} Styling options.
*/
get style() {
if ( PaymentContext.MiniCart === this.context ) {
return this.#styles.MiniCart;
}
return this.#styles.Default;
}
/**
* Returns the context-relevant wrapper ID.
*
* @readonly
* @return {string} The wrapper-element's ID (without the `#` prefix).
*/
get wrapperId() {
if ( PaymentContext.MiniCart === this.context ) {
return this.wrappers.MiniCart;
} else if ( this.isSeparateGateway ) {
return this.wrappers.Gateway;
} else if ( PaymentContext.Blocks.includes( this.context ) ) {
return this.wrappers.Block;
}
return this.wrappers.Default;
}
/**
* Determines if the current payment button should be rendered as a stand-alone gateway.
* The return value `false` usually means, that the payment button is bundled with all available
* payment buttons.
*
* The decision depends on the button context (placement) and the plugin settings.
*
* @return {boolean} True, if the current button represents a stand-alone gateway.
*/
get isSeparateGateway() {
return (
this.#buttonConfig.is_wc_gateway_enabled &&
PaymentContext.Gateways.includes( this.context )
);
}
/**
* Whether the currently selected payment gateway is set to the payment method.
*
* Only relevant on checkout pages, when `this.isSeparateGateway` is true.
*
* @return {boolean} True means that this payment method is selected as current gateway.
*/
get isCurrentGateway() {
if ( ! this.isSeparateGateway ) {
return false;
}
/*
* We need to rely on `getCurrentPaymentMethod()` here, as the `CheckoutBootstrap.js`
* module fires the "ButtonEvents.RENDER" event before any PaymentButton instances are
* created. I.e. we cannot observe the initial gateway selection event.
*/
return this.methodId === getCurrentPaymentMethod();
}
/**
* Flags a preview button without actual payment logic.
*
* @return {boolean} True indicates a preview instance that has no payment logic.
*/
get isPreview() {
return PaymentContext.Preview === this.context;
}
/**
* Whether the browser can accept this payment method.
*
* @return {boolean} True, if payments are technically possible.
*/
get isEligible() {
return this.#isEligible;
}
/**
* Changes the eligibility state of this button component.
*
* @param {boolean} newState Whether the browser can accept payments.
*/
set isEligible( newState ) {
if ( newState === this.#isEligible ) {
return;
}
this.#isEligible = newState;
this.triggerRedraw();
}
/**
* The visibility state of the button.
* This flag does not reflect actual visibility on the page, but rather, if the button
* is intended/allowed to be displayed, in case all other checks pass.
*
* @return {boolean} True indicates, that the button can be displayed.
*/
get isVisible() {
return this.#isVisible;
}
/**
* Change the visibility of the button.
*
* A visible button does not always force the button to render on the page. It only means, that
* the button is allowed or not allowed to render, if certain other conditions are met.
*
* @param {boolean} newState Whether rendering the button is allowed.
*/
set isVisible( newState ) {
if ( this.#isVisible === newState ) {
return;
}
this.#isVisible = newState;
this.triggerRedraw();
}
/**
* Returns the HTML element that wraps the current button
*
* @readonly
* @return {HTMLElement|null} The wrapper element, or null.
*/
get wrapperElement() {
return document.getElementById( this.wrapperId );
}
/**
* Checks whether the main button-wrapper is present in the current DOM.
*
* @readonly
* @return {boolean} True, if the button context (wrapper element) is found.
*/
get isPresent() {
return this.wrapperElement instanceof HTMLElement;
}
/**
* Checks, if the payment button is still attached to the DOM.
*
* WooCommerce performs some partial reloads in many cases, which can lead to our payment
* button
* to move into the browser's memory. In that case, we need to recreate the button in the
* updated DOM.
*
* @return {boolean} True means, the button is still present (and typically visible) on the
* page.
*/
get isButtonAttached() {
if ( ! this.#button ) {
return false;
}
let parent = this.#button.parentElement;
while ( parent?.parentElement ) {
if ( 'BODY' === parent.tagName ) {
return true;
}
parent = parent.parentElement;
}
return false;
}
/**
* Log a debug detail to the browser console.
*
* @param {any} args
*/
log( ...args ) {
this.#logger.log( ...args );
}
/**
* Log an error message to the browser console.
*
* @param {any} args
*/
error( ...args ) {
this.#logger.error( ...args );
}
/**
* Determines if the current button instance has valid and complete configuration details.
* Used during initialization to decide if the button can be initialized or should be skipped.
*
* Can be implemented by the derived class.
*
* @param {boolean} [silent=false] - Set to true to suppress console errors.
* @return {boolean} True indicates the config is valid and initialization can continue.
*/
validateConfiguration( silent = false ) {
return true;
}
applyButtonStyles( buttonConfig, ppcpConfig = null ) {
if ( ! ppcpConfig ) {
ppcpConfig = this.ppcpConfig;
}
this.#styles = this.constructor.getStyles( buttonConfig, ppcpConfig );
if ( this.isInitialized ) {
this.triggerRedraw();
}
}
/**
* Configures the button instance. Must be called before the initial `init()`.
*
* Parameters are defined by the derived class.
*
* @abstract
*/
configure() {}
/**
* Must be named `init()` to simulate "protected" visibility:
* Since the derived class also implements a method with the same name, this method can only
* be called by the derived class, but not from any other code.
*/
init() {
this.#isInitialized = true;
}
/**
* Must be named `reinit()` to simulate "protected" visibility:
* Since the derived class also implements a method with the same name, this method can only
* be called by the derived class, but not from any other code.
*/
reinit() {
this.#isInitialized = false;
this.#isEligible = false;
}
triggerRedraw() {
this.showPaymentGateway();
dispatchButtonEvent( {
event: ButtonEvents.REDRAW,
paymentMethod: this.methodId,
} );
}
/**
* Attaches event listeners to show or hide the payment button when needed.
*/
initEventListeners() {
// Refresh the button - this might show, hide or re-create the payment button.
observeButtonEvent( {
event: ButtonEvents.REDRAW,
paymentMethod: this.methodId,
callback: () => this.refresh(),
} );
// Events relevant for buttons inside a payment gateway.
if ( PaymentContext.Gateways.includes( this.context ) ) {
const parentMethod = this.isSeparateGateway
? this.methodId
: PaymentMethods.PAYPAL;
// Hide the button right after the user selected _any_ gateway.
observeButtonEvent( {
event: ButtonEvents.INVALIDATE,
callback: () => ( this.isVisible = false ),
} );
// Show the button (again) when the user selected the current gateway.
observeButtonEvent( {
event: ButtonEvents.RENDER,
paymentMethod: parentMethod,
callback: () => ( this.isVisible = true ),
} );
}
}
/**
* Refreshes the payment button on the page.
*/
refresh() {
if ( ! this.isPresent ) {
return;
}
this.applyWrapperStyles();
if ( this.isEligible && this.isPresent && this.isVisible ) {
if ( ! this.isButtonAttached ) {
this.log( 'refresh.addButton' );
this.addButton();
}
}
}
/**
* Makes the custom payment gateway visible by removing initial inline styles from the DOM.
*
* Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true`
*/
showPaymentGateway() {
if ( ! this.isSeparateGateway || ! this.isEligible ) {
return;
}
const styleSelectors = `style[data-hide-gateway="${ this.methodId }"]`;
const styles = document.querySelectorAll( styleSelectors );
if ( ! styles.length ) {
return;
}
this.log( 'Show gateway' );
styles.forEach( ( el ) => el.remove() );
// This code runs only once, during button initialization, and fixes the initial visibility.
this.isVisible = this.isCurrentGateway;
}
/**
* Applies CSS classes and inline styling to the payment button wrapper.
*/
applyWrapperStyles() {
const wrapper = this.wrapperElement;
const { shape, height } = this.style;
for ( const classItem of this.#appliedClasses ) {
wrapper.classList.remove( classItem );
}
this.#appliedClasses = [];
const newClasses = [
`ppcp-button-${ shape }`,
'ppcp-button-apm',
this.cssClass,
];
wrapper.classList.add( ...newClasses );
this.#appliedClasses.push( ...newClasses );
if ( height ) {
wrapper.style.height = `${ height }px`;
}
// Apply the wrapper visibility.
wrapper.style.display = this.isVisible ? 'block' : 'none';
}
/**
* Creates a new payment button (HTMLElement) and must call `this.insertButton()` to display
* that button in the correct wrapper.
*
* @abstract
*/
addButton() {
throw new Error( 'Must be implemented by the child class' );
}
/**
* Prepares the button wrapper element and inserts the provided payment button into the DOM.
*
* If a payment button was previously inserted to the wrapper, calling this method again will
* first remove the previous button.
*
* @param {HTMLElement} button - The button element to inject.
*/
insertButton( button ) {
if ( ! this.isPresent ) {
return;
}
const wrapper = this.wrapperElement;
if ( this.#button ) {
this.removeButton();
}
this.log( 'addButton', button );
this.#button = button;
wrapper.appendChild( this.#button );
}
/**
* Removes the payment button from the DOM.
*/
removeButton() {
if ( ! this.isPresent || ! this.#button ) {
return;
}
this.log( 'removeButton' );
try {
this.wrapperElement.removeChild( this.#button );
} catch ( Exception ) {
// Ignore this.
}
this.#button = null;
}
}

View file

@ -1,11 +1,17 @@
import { loadCustomScript } from '@paypal/paypal-js';
import widgetBuilder from './WidgetBuilder';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
/**
* Manages all PreviewButton instances of a certain payment method on the page.
*/
class PreviewButtonManager {
/**
* @type {ConsoleLogger}
*/
#logger;
/**
* Resolves the promise.
* Used by `this.boostrap()` to process enqueued initialization logic.
@ -32,6 +38,9 @@ class PreviewButtonManager {
this.apiConfig = null;
this.apiError = '';
this.#logger = new ConsoleLogger( this.methodName, 'preview-manager' );
this.#logger.enabled = true; // Manually set this to true for development.
this.#onInit = new Promise( ( resolve ) => {
this.#onInitResolver = resolve;
} );
@ -61,9 +70,11 @@ class PreviewButtonManager {
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @abstract
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
// eslint-disable-next-line no-unused-vars
async fetchConfig( payPal ) {
throw new Error(
'The "fetchConfig" method must be implemented by the derived class'
@ -74,9 +85,11 @@ class PreviewButtonManager {
* Protected method that needs to be implemented by the derived class.
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @abstract
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {PreviewButton}
*/
// eslint-disable-next-line no-unused-vars
createButtonInstance( wrapperId ) {
throw new Error(
'The "createButtonInstance" method must be implemented by the derived class'
@ -90,7 +103,7 @@ class PreviewButtonManager {
* This dummy is only visible on the admin side, and not rendered on the front-end.
*
* @todo Consider refactoring this into a new class that extends the PreviewButton class.
* @param wrapperId
* @param {string} wrapperId
* @return {any}
*/
createDummy( wrapperId ) {
@ -128,13 +141,24 @@ class PreviewButtonManager {
);
}
/**
* Output a debug message to the console, with a module-specific prefix.
*
* @param {string} message - Log message.
* @param {...any} args - Optional. Additional args to output.
*/
log( message, ...args ) {
this.#logger.log( message, ...args );
}
/**
* Output an error message to the console, with a module-specific prefix.
* @param message
* @param {...any} args
*
* @param {string} message - Log message.
* @param {...any} args - Optional. Additional args to output.
*/
error( message, ...args ) {
console.error( `${ this.methodName } ${ message }`, ...args );
this.#logger.error( message, ...args );
}
/**
@ -242,21 +266,21 @@ class PreviewButtonManager {
}
if ( ! this.shouldInsertPreviewButton( id ) ) {
this.log( 'Skip preview rendering for this preview-box', id );
return;
}
if ( ! this.buttons[ id ] ) {
this._addButton( id, ppcpConfig );
} else {
// This is a debounced method, that fires after 100ms.
this._configureAllButtons( ppcpConfig );
this._configureButton( id, ppcpConfig );
}
}
/**
* Determines if the preview box supports the current button.
*
* When this function returns false, this manager instance does not create a new preview button.
* E.g. "Should the current preview-box display Google Pay buttons?"
*
* @param {string} previewId - ID of the inner preview box container.
* @return {boolean} True if the box is eligible for the preview button, false otherwise.
@ -271,10 +295,14 @@ class PreviewButtonManager {
/**
* Applies a new configuration to an existing preview button.
*
* @private
* @param id
* @param ppcpConfig
*/
_configureButton( id, ppcpConfig ) {
this.log( 'configureButton', id, ppcpConfig );
this.buttons[ id ]
.setDynamic( this.isDynamic() )
.setPpcpConfig( ppcpConfig )
@ -283,9 +311,13 @@ class PreviewButtonManager {
/**
* Apples the provided configuration to all existing preview buttons.
* @param ppcpConfig
*
* @private
* @param ppcpConfig - The new styling to use for the preview buttons.
*/
_configureAllButtons( ppcpConfig ) {
this.log( 'configureAllButtons', ppcpConfig );
Object.entries( this.buttons ).forEach( ( [ id, button ] ) => {
this._configureButton( id, {
...ppcpConfig,
@ -302,13 +334,20 @@ class PreviewButtonManager {
/**
* Creates a new preview button, that is rendered once the bootstrapping Promise resolves.
* @param id
* @param ppcpConfig
*
* @private
* @param id - The button to add.
* @param ppcpConfig - The styling to apply to the preview button.
*/
_addButton( id, ppcpConfig ) {
this.log( 'addButton', id, ppcpConfig );
const createButton = () => {
if ( ! this.buttons[ id ] ) {
this.log( 'createButton.new', id );
let newInst;
if ( this.apiConfig && 'object' === typeof this.apiConfig ) {
newInst = this.createButtonInstance( id ).setButtonConfig(
this.buttonConfig

View file

@ -139,7 +139,7 @@ class Renderer {
};
// Check the condition and add the handler if needed
if ( this.defaultSettings.should_handle_shipping_in_paypal ) {
if ( this.shouldEnableShippingCallback() ) {
options.onShippingOptionsChange = ( data, actions ) => {
let shippingOptionsChange =
! this.isVenmoButtonClickedWhenVaultingIsEnabled(
@ -227,6 +227,12 @@ class Renderer {
return venmoButtonClicked && this.defaultSettings.vaultingEnabled;
};
shouldEnableShippingCallback = () => {
console.log(this.defaultSettings.context, this.defaultSettings)
let 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 ?? '' ) );
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Assets;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Tokens;
use WC_Product;
use WC_Product_Variation;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
@ -1292,9 +1293,11 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'early_checkout_validation_enabled' => $this->early_validation_enabled,
'funding_sources_without_redirect' => $this->funding_sources_without_redirect,
'user' => array(
'is_logged' => is_user_logged_in(),
'is_logged' => is_user_logged_in(),
'has_wc_card_payment_tokens' => $this->user_has_wc_card_payment_tokens( get_current_user_id() ),
),
'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(),
'needShipping' => WC()->cart->needs_shipping(),
'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ),
);
@ -2132,4 +2135,19 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
return $location;
}
}
/**
* Whether the given user has WC card payment tokens.
*
* @param int $user_id The user ID.
* @return bool
*/
private function user_has_wc_card_payment_tokens( int $user_id ): bool {
$tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID );
if ( $tokens ) {
return true;
}
return false;
}
}

View file

@ -9,8 +9,10 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use Exception;
use RuntimeException;
use WC_Cart;
use WC_Data_Exception;
use WC_Order;
use WC_Order_Item_Product;
use WC_Order_Item_Shipping;
@ -85,17 +87,24 @@ class WooCommerceOrderCreator {
throw new RuntimeException( 'Problem creating WC order.' );
}
$payer = $order->payer();
$shipping = $order->purchase_units()[0]->shipping();
try {
$payer = $order->payer();
$shipping = $order->purchase_units()[0]->shipping();
$this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order );
$this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping );
$this->configure_shipping( $wc_order, $payer, $shipping );
$this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() );
$this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order );
$this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping );
$this->configure_shipping( $wc_order, $payer, $shipping, $wc_cart );
$this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() );
$wc_order->calculate_totals();
$wc_order->save();
$wc_order->calculate_totals();
$wc_order->save();
} catch ( Exception $exception ) {
$wc_order->delete( true );
throw new RuntimeException( 'Failed to create WooCommerce order: ' . $exception->getMessage() );
}
do_action( 'woocommerce_paypal_payments_shipping_callback_woocommerce_order_created', $wc_order, $wc_cart );
return $wc_order;
}
@ -153,7 +162,7 @@ class WooCommerceOrderCreator {
$item->set_total( $subscription_total );
$subscription->add_product( $product );
$this->configure_shipping( $subscription, $payer, $shipping );
$this->configure_shipping( $subscription, $payer, $shipping, $wc_cart );
$this->configure_payment_source( $subscription );
$this->configure_coupons( $subscription, $wc_cart->get_applied_coupons() );
@ -178,9 +187,11 @@ class WooCommerceOrderCreator {
* @param WC_Order $wc_order The WC order.
* @param Payer|null $payer The payer.
* @param Shipping|null $shipping The shipping.
* @param WC_Cart $wc_cart The Cart.
* @return void
* @throws WC_Data_Exception|RuntimeException When failing to configure shipping.
*/
protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping ): void {
protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping, WC_Cart $wc_cart ): void {
$shipping_address = null;
$billing_address = null;
$shipping_options = null;
@ -218,6 +229,10 @@ class WooCommerceOrderCreator {
$shipping_options = $shipping->options()[0] ?? '';
}
if ( $wc_cart->needs_shipping() && empty( $shipping_options ) ) {
throw new RuntimeException( 'No shipping method has been selected.' );
}
if ( $shipping_address ) {
$wc_order->set_shipping_address( $shipping_address );
}

View file

@ -0,0 +1,31 @@
{
"name": "ppcp-card-fields",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,47 @@
import { cardFieldStyles } from './CardFieldsHelper';
export function renderFields( cardFields ) {
const nameField = document.getElementById(
'ppcp-credit-card-gateway-card-name'
);
if ( nameField && nameField.hidden !== true ) {
const styles = cardFieldStyles( nameField );
cardFields
.NameField( { style: { input: styles } } )
.render( nameField.parentNode );
nameField.hidden = true;
}
const numberField = document.getElementById(
'ppcp-credit-card-gateway-card-number'
);
if ( numberField && numberField.hidden !== true ) {
const styles = cardFieldStyles( numberField );
cardFields
.NumberField( { style: { input: styles } } )
.render( numberField.parentNode );
numberField.hidden = true;
}
const expiryField = document.getElementById(
'ppcp-credit-card-gateway-card-expiry'
);
if ( expiryField && expiryField.hidden !== true ) {
const styles = cardFieldStyles( expiryField );
cardFields
.ExpiryField( { style: { input: styles } } )
.render( expiryField.parentNode );
expiryField.hidden = true;
}
const cvvField = document.getElementById(
'ppcp-credit-card-gateway-card-cvc'
);
if ( cvvField && cvvField.hidden !== true ) {
const styles = cardFieldStyles( cvvField );
cardFields
.CVVField( { style: { input: styles } } )
.render( cvvField.parentNode );
cvvField.hidden = true;
}
}

View file

@ -17,795 +17,51 @@ return array(
$save_payment_methods_applies = $container->get( 'card-fields.helpers.save-payment-methods-applies' );
assert( $save_payment_methods_applies instanceof CardFieldsApplies );
return $save_payment_methods_applies->for_country_currency();
return $save_payment_methods_applies->for_country();
},
'card-fields.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : CardFieldsApplies {
return new CardFieldsApplies(
$container->get( 'card-fields.supported-country-currency-matrix' ),
$container->get( 'api.shop.currency' ),
$container->get( 'card-fields.supported-country-matrix' ),
$container->get( 'api.shop.country' )
);
},
'card-fields.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
'card-fields.supported-country-matrix' => static function ( ContainerInterface $container ) : array {
return apply_filters(
'woocommerce_paypal_payments_card_fields_supported_country_currency_matrix',
'woocommerce_paypal_payments_card_fields_supported_country_matrix',
array(
'AU' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'AT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'BG' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'CA' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'CN' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'CY' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'CZ' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'DK' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'EE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'FI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'FR' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'DE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'GR' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'HU' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'IT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LV' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'LU' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'MT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'NL' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'PL' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'PT' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'RO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'SK' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'SI' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'ES' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'SE' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'GB' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'US' => array(
'AUD',
'CAD',
'EUR',
'GBP',
'JPY',
'USD',
),
'NO' => array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
),
'AU',
'AT',
'BE',
'BG',
'CA',
'CN',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'GR',
'HU',
'IE',
'IT',
'LV',
'LI',
'LT',
'LU',
'MT',
'NL',
'PL',
'PT',
'RO',
'SK',
'SI',
'ES',
'SE',
'GB',
'US',
'NO',
)
);
},

View file

@ -1,6 +1,6 @@
<?php
/**
* Service for checking whether Card Fields can be used in the current country and the current currency.
* Service for checking whether Card Fields can be used in the current country.
*
* @package WooCommerce\PayPalCommerce\CardFields\Helper
*/
@ -15,18 +15,11 @@ namespace WooCommerce\PayPalCommerce\CardFields\Helper;
class CardFieldsApplies {
/**
* The matrix which countries and currency combinations can be used.
* The matrix which countries can be used.
*
* @var array
*/
private $allowed_country_currency_matrix;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
private $allowed_country_matrix;
/**
* 2-letter country code of the shop.
@ -38,29 +31,23 @@ class CardFieldsApplies {
/**
* CardFieldsApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used.
* @param string $currency 3-letter currency code of the shop.
* @param array $allowed_country_matrix The matrix which countries can be used.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
array $allowed_country_currency_matrix,
string $currency,
array $allowed_country_matrix,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
$this->allowed_country_matrix = $allowed_country_matrix;
$this->country = $country;
}
/**
* Returns whether Card Fields can be used in the current country and the current currency.
* Returns whether Card Fields can be used in the current country.
*
* @return bool
*/
public function for_country_currency(): bool {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
public function for_country(): bool {
return in_array( $this->country, $this->allowed_country_matrix, true );
}
}

View file

@ -0,0 +1,38 @@
const path = require( 'path' );
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {
render: path.resolve( './resources/js/Render.js' ),
helper: path.resolve( './resources/js/CardFieldsHelper.js' ),
},
output: {
path: path.resolve( __dirname, 'assets/' ),
filename: 'js/[name].js',
},
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
},
},
{ loader: 'sass-loader' },
],
},
],
},
};

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,8 @@ document.addEventListener( 'DOMContentLoaded', () => {
);
const wcShipmentTaxBuyLabelButtonSelector =
'.components-modal__screen-overlay .label-purchase-modal__sidebar .purchase-section button.components-button';
const dhlGenerateLabelButton =
document.getElementById( 'dhl-label-button' );
const toggleLoaderVisibility = function () {
const loader = document.querySelector( '.ppcp-tracking-loader' );
@ -44,6 +46,20 @@ document.addEventListener( 'DOMContentLoaded', () => {
}
};
const waitForButtonRemoval = function ( button ) {
if ( document.body.contains( button ) ) {
setTimeout( () => waitForButtonRemoval( button ), 100 );
} else {
jQuery( orderTrackingContainerSelector ).load(
loadLocation,
'',
function () {
toggleLoaderVisibility();
}
);
}
};
if (
gzdSyncEnabled &&
typeof gzdSaveButton !== 'undefined' &&
@ -66,10 +82,30 @@ document.addEventListener( 'DOMContentLoaded', () => {
} );
}
if (
typeof dhlGenerateLabelButton !== 'undefined' &&
dhlGenerateLabelButton != null
) {
dhlGenerateLabelButton.addEventListener( 'click', function ( event ) {
toggleLoaderVisibility();
waitForButtonRemoval( dhlGenerateLabelButton );
} );
}
jQuery( document ).on(
'mouseover mouseout',
'#dhl_delete_label',
function ( event ) {
jQuery( '#ppcp-shipment-status' )
.val( 'CANCELLED' )
.trigger( 'change' );
document.querySelector( '.update_shipment' ).click();
}
);
if (
wcShippingTaxSyncEnabled &&
typeof wcShippingTaxSyncEnabled !== 'undefined' &&
wcShippingTaxSyncEnabled != null
typeof wcShippingTaxSyncEnabled !== 'undefined'
) {
document.addEventListener( 'click', function ( event ) {
const wcShipmentTaxBuyLabelButton = event.target.closest(

View file

@ -77,6 +77,9 @@ return array(
'compat.ywot.is_supported_plugin_version_active' => function (): bool {
return function_exists( 'yith_ywot_init' );
},
'compat.dhl.is_supported_plugin_version_active' => function (): bool {
return function_exists( 'PR_DHL' );
},
'compat.shipstation.is_supported_plugin_version_active' => function (): bool {
return function_exists( 'woocommerce_shipstation_init' );
},
@ -86,6 +89,9 @@ return array(
'compat.nyp.is_supported_plugin_version_active' => function (): bool {
return function_exists( 'wc_nyp_init' );
},
'compat.wc_bookings.is_supported_plugin_version_active' => function (): bool {
return class_exists( 'WC_Bookings' );
},
'compat.module.url' => static function ( ContainerInterface $container ): string {
/**

View file

@ -9,6 +9,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Cart;
use WC_Order;
use WC_Order_Item_Product;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -66,6 +71,14 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
if ( $is_nyp_active ) {
$this->initialize_nyp_compat_layer();
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$is_wc_bookings_active = $c->get( 'compat.wc_bookings.is_supported_plugin_version_active' );
if ( $is_wc_bookings_active ) {
$this->initialize_wc_bookings_compat_layer( $logger );
}
return true;
}
@ -410,4 +423,62 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
2
);
}
/**
* Sets up the compatibility layer for WooCommerce Bookings plugin.
*
* @param LoggerInterface $logger The logger.
* @return void
*/
protected function initialize_wc_bookings_compat_layer( LoggerInterface $logger ): void {
add_action(
'woocommerce_paypal_payments_shipping_callback_woocommerce_order_created',
static function ( WC_Order $wc_order, WC_Cart $wc_cart ) use ( $logger ): void {
try {
$cart_contents = $wc_cart->get_cart();
foreach ( $cart_contents as $cart_item ) {
if ( empty( $cart_item['booking'] ) ) {
continue;
}
foreach ( $wc_order->get_items() as $wc_order_item ) {
if ( ! is_a( $wc_order_item, WC_Order_Item_Product::class ) ) {
continue;
}
$product_id = $wc_order_item->get_variation_id() ?: $wc_order_item->get_product_id();
$product = wc_get_product( $product_id );
if ( ! is_wc_booking_product( $product ) ) {
continue;
}
$booking_data = array(
'cost' => $cart_item['booking']['_cost'] ?? 0,
'start_date' => $cart_item['booking']['_start_date'] ?? 0,
'end_date' => $cart_item['booking']['_end_date'] ?? 0,
'all_day' => $cart_item['booking']['_all_day'] ?? 0,
'local_timezone' => $cart_item['booking']['_local_timezone'] ?? 0,
'order_item_id' => $wc_order_item->get_id(),
);
if ( isset( $cart_item['booking']['_resource_id'] ) ) {
$booking_data['resource_id'] = $cart_item['booking']['_resource_id'];
}
if ( isset( $cart_item['booking']['_persons'] ) ) {
$booking_data['persons'] = $cart_item['booking']['_persons'];
}
create_wc_booking( $cart_item['product_id'], $booking_data, 'unpaid' );
}
}
} catch ( Exception $exception ) {
$logger->warning( 'Failed to create booking for WooCommerce Bookings plugin: ' . $exception->getMessage() );
}
},
10,
2
);
}
}

View file

@ -166,7 +166,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'default' => 'plain',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',

View file

@ -1,3 +1,10 @@
/* Front end display */
.ppcp-button-apm .gpay-card-info-container-fill .gpay-card-info-container {
outline-offset: -1px;
border-radius: var(--apm-button-border-radius);
}
/* Admin preview */
.ppcp-button-googlepay {
min-height: 40px;

View file

@ -1,10 +1,120 @@
import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import {
combineStyles,
combineWrapperIds,
} from '../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
/**
* Plugin-specific styling.
*
* Note that most properties of this object do not apply to the Google Pay button.
*
* @typedef {Object} PPCPStyle
* @property {string} shape - Outline shape.
* @property {?number} height - Button height in pixel.
*/
/**
* Style options that are defined by the Google Pay SDK and are required to render the button.
*
* @typedef {Object} GooglePayStyle
* @property {string} type - Defines the button label.
* @property {string} color - Button color
* @property {string} language - The locale; an empty string will apply the user-agent's language.
*/
/**
* Google Pay JS SDK
*
* @see https://developers.google.com/pay/api/web/reference/request-objects
* @typedef {Object} GooglePaySDK
* @property {typeof PaymentsClient} PaymentsClient - Main API client for payment actions.
*/
/**
* The Payments Client class, generated by the Google Pay SDK.
*
* @see https://developers.google.com/pay/api/web/reference/client
* @typedef {Object} PaymentsClient
* @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage.
* @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API.
* @property {Function} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters
* @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet.
* @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options.
*/
/**
* This object describes the transaction details.
*
* @see https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo
* @typedef {Object} TransactionInfo
* @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code.
* @property {string} countryCode - Optional. required for EEA countries,
* @property {string} transactionId - Optional. A unique ID that identifies a facilitation attempt. Highly encouraged for troubleshooting.
* @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used:
* @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places.
* @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.).
* @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items.
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet.
*/
class GooglepayButton extends PaymentButton {
/**
* @inheritDoc
*/
static methodId = PaymentMethods.GOOGLEPAY;
/**
* @inheritDoc
*/
static cssClass = 'google-pay';
/**
* Client reference, provided by the Google Pay JS SDK.
*/
#paymentsClient = null;
/**
* Details about the processed transaction.
*
* @type {?TransactionInfo}
*/
#transactionInfo = null;
googlePayConfig = null;
/**
* @inheritDoc
*/
static getWrappers( buttonConfig, ppcpConfig ) {
return combineWrapperIds(
buttonConfig?.button?.wrapper || '',
buttonConfig?.button?.mini_cart_wrapper || '',
ppcpConfig?.button?.wrapper || '',
'ppc-button-googlepay-container',
'ppc-button-ppcp-googlepay'
);
}
/**
* @inheritDoc
*/
static getStyles( buttonConfig, ppcpConfig ) {
const styles = combineStyles(
ppcpConfig?.button || {},
buttonConfig?.button || {}
);
if ( 'buy' === styles.MiniCart.type ) {
styles.MiniCart.type = 'pay';
}
return styles;
}
class GooglepayButton {
constructor(
context,
externalHandler,
@ -12,274 +122,257 @@ class GooglepayButton {
ppcpConfig,
contextHandler
) {
apmButtonsInit( ppcpConfig );
// Disable debug output in the browser console:
// buttonConfig.is_debug = false;
this.isInitialized = false;
super(
context,
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
);
this.context = context;
this.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.contextHandler = contextHandler;
this.init = this.init.bind( this );
this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this );
this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this );
this.onButtonClick = this.onButtonClick.bind( this );
this.paymentsClient = null;
this.log( 'Create instance' );
}
this.log = function () {
if ( this.buttonConfig.is_debug ) {
//console.log('[GooglePayButton]', ...arguments);
/**
* @inheritDoc
*/
get requiresShipping() {
return super.requiresShipping && this.buttonConfig.shipping?.enabled;
}
/**
* The Google Pay API.
*
* @return {?GooglePaySDK} API for the Google Pay JS SDK, or null when SDK is not ready yet.
*/
get googlePayApi() {
return window.google?.payments?.api;
}
/**
* The Google Pay PaymentsClient instance created by this button.
* @see https://developers.google.com/pay/api/web/reference/client
*
* @return {?PaymentsClient} The SDK object, or null when SDK is not ready yet.
*/
get paymentsClient() {
return this.#paymentsClient;
}
/**
* Details about the processed transaction.
*
* This object defines the price that is charged, and text that is displayed inside the
* payment sheet.
*
* @return {?TransactionInfo} The TransactionInfo object.
*/
get transactionInfo() {
return this.#transactionInfo;
}
/**
* Assign the new transaction details to the payment button.
*
* @param {TransactionInfo} newTransactionInfo - Transaction details.
*/
set transactionInfo( newTransactionInfo ) {
this.#transactionInfo = newTransactionInfo;
this.refresh();
}
/**
* @inheritDoc
*/
validateConfiguration( silent = false ) {
const validEnvs = [ 'PRODUCTION', 'TEST' ];
const isInvalid = ( ...args ) => {
if ( ! silent ) {
this.error( ...args );
}
return false;
};
}
init( config, transactionInfo ) {
if ( this.isInitialized ) {
return;
}
this.isInitialized = true;
if ( ! this.validateConfig() ) {
return;
if ( ! validEnvs.includes( this.buttonConfig.environment ) ) {
return isInvalid(
'Invalid environment:',
this.buttonConfig.environment
);
}
if ( ! this.contextHandler.validateContext() ) {
return;
// Preview buttons only need a valid environment.
if ( this.isPreview ) {
return true;
}
this.googlePayConfig = config;
this.transactionInfo = transactionInfo;
this.allowedPaymentMethods = config.allowedPaymentMethods;
this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
this.initClient();
this.initEventHandlers();
this.paymentsClient
.isReadyToPay(
this.buildReadyToPayRequest(
this.allowedPaymentMethods,
config
)
)
.then( ( response ) => {
if ( response.result ) {
if (
( this.context === 'checkout' ||
this.context === 'pay-now' ) &&
this.buttonConfig.is_wc_gateway_enabled === '1'
) {
const wrapper = document.getElementById(
'ppc-button-ppcp-googlepay'
);
if ( wrapper ) {
const { ppcpStyle, buttonStyle } =
this.contextConfig();
wrapper.classList.add(
`ppcp-button-${ ppcpStyle.shape }`,
'ppcp-button-apm',
'ppcp-button-googlepay'
);
if ( ppcpStyle.height ) {
wrapper.style.height = `${ ppcpStyle.height }px`;
}
this.addButtonCheckout(
this.baseCardPaymentMethod,
wrapper,
buttonStyle
);
return;
}
}
this.addButton( this.baseCardPaymentMethod );
}
} )
.catch( function ( err ) {
console.error( err );
} );
}
reinit() {
if ( ! this.googlePayConfig ) {
return;
return isInvalid(
'No API configuration - missing configure() call?'
);
}
this.isInitialized = false;
this.init( this.googlePayConfig, this.transactionInfo );
}
validateConfig() {
if (
[ 'PRODUCTION', 'TEST' ].indexOf(
this.buttonConfig.environment
) === -1
) {
console.error(
'[GooglePayButton] Invalid environment.',
this.buttonConfig.environment
if ( ! this.transactionInfo ) {
return isInvalid(
'No transactionInfo - missing configure() call?'
);
return false;
}
if ( ! this.contextHandler ) {
console.error(
'[GooglePayButton] Invalid context handler.',
this.contextHandler
);
return false;
if ( ! typeof this.contextHandler?.validateContext() ) {
return isInvalid( 'Invalid context handler.', this.contextHandler );
}
return true;
}
/**
* Returns configurations relative to this button context.
* Configures the button instance. Must be called before the initial `init()`.
*
* @param {Object} apiConfig - API configuration.
* @param {Object} transactionInfo - Transaction details; required before "init" call.
*/
contextConfig() {
const config = {
wrapper: this.buttonConfig.button.wrapper,
ppcpStyle: this.ppcpConfig.button.style,
buttonStyle: this.buttonConfig.button.style,
ppcpButtonWrapper: this.ppcpConfig.button.wrapper,
};
configure( apiConfig, transactionInfo ) {
this.googlePayConfig = apiConfig;
this.#transactionInfo = transactionInfo;
if ( this.context === 'mini-cart' ) {
config.wrapper = this.buttonConfig.button.mini_cart_wrapper;
config.ppcpStyle = this.ppcpConfig.button.mini_cart_style;
config.buttonStyle = this.buttonConfig.button.mini_cart_style;
config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper;
// Handle incompatible types.
if ( config.buttonStyle.type === 'buy' ) {
config.buttonStyle.type = 'pay';
}
}
if (
[ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1
) {
config.ppcpButtonWrapper =
'#express-payment-method-ppcp-gateway-paypal';
}
return config;
this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods;
this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
}
initClient() {
const callbacks = {
onPaymentAuthorized: this.onPaymentAuthorized.bind( this ),
};
if (
this.buttonConfig.shipping.enabled &&
this.contextHandler.shippingAllowed()
) {
callbacks.onPaymentDataChanged =
this.onPaymentDataChanged.bind( this );
init() {
// Use `reinit()` to force a full refresh of an initialized button.
if ( this.isInitialized ) {
return;
}
this.paymentsClient = new google.payments.api.PaymentsClient( {
// Stop, if configuration is invalid.
if ( ! this.validateConfiguration() ) {
return;
}
super.init();
this.#paymentsClient = this.createPaymentsClient();
if ( ! this.isPresent ) {
this.log( 'Payment wrapper not found', this.wrapperId );
return;
}
if ( ! this.paymentsClient ) {
this.log( 'Could not initialize the payments client' );
return;
}
this.paymentsClient
.isReadyToPay(
this.buildReadyToPayRequest(
this.allowedPaymentMethods,
this.googlePayConfig
)
)
.then( ( response ) => {
this.log( 'PaymentsClient.isReadyToPay response:', response );
this.isEligible = !! response.result;
} )
.catch( ( err ) => {
this.error( err );
this.isEligible = false;
} );
}
reinit() {
// Missing (invalid) configuration indicates, that the first `init()` call did not happen yet.
if ( ! this.validateConfiguration( true ) ) {
return;
}
super.reinit();
this.init();
}
/**
* Provides an object with relevant paymentDataCallbacks for the current button instance.
*
* @return {Object} An object containing callbacks for the current scope & configuration.
*/
preparePaymentDataCallbacks() {
const callbacks = {};
// We do not attach any callbacks to preview buttons.
if ( this.isPreview ) {
return callbacks;
}
callbacks.onPaymentAuthorized = this.onPaymentAuthorized;
if ( this.requiresShipping ) {
callbacks.onPaymentDataChanged = this.onPaymentDataChanged;
}
return callbacks;
}
createPaymentsClient() {
if ( ! this.googlePayApi ) {
return null;
}
const callbacks = this.preparePaymentDataCallbacks();
/**
* Consider providing merchant info here:
*
* @see https://developers.google.com/pay/api/web/reference/request-objects#PaymentOptions
*/
return new this.googlePayApi.PaymentsClient( {
environment: this.buttonConfig.environment,
// add merchant info maybe
paymentDataCallbacks: callbacks,
} );
}
initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
if ( wrapper === ppcpButtonWrapper ) {
throw new Error(
`[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
);
}
const syncButtonVisibility = () => {
const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
setEnabled(
wrapper,
! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
);
};
jQuery( document ).on(
'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
( ev, data ) => {
if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
syncButtonVisibility();
}
}
);
syncButtonVisibility();
}
buildReadyToPayRequest( allowedPaymentMethods, baseRequest ) {
this.log( 'Ready To Pay request', baseRequest, allowedPaymentMethods );
return Object.assign( {}, baseRequest, {
allowedPaymentMethods,
} );
}
/**
* Add a Google Pay purchase button
* @param baseCardPaymentMethod
* Creates the payment button and calls `this.insertButton()` to make the button visible in the
* correct wrapper.
*/
addButton( baseCardPaymentMethod ) {
this.log( 'addButton', this.context );
addButton() {
if ( ! this.paymentsClient ) {
return;
}
const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig();
const baseCardPaymentMethod = this.baseCardPaymentMethod;
const { color, type, language } = this.style;
this.waitForWrapper( wrapper, () => {
jQuery( wrapper ).addClass( 'ppcp-button-' + ppcpStyle.shape );
if ( ppcpStyle.height ) {
jQuery( wrapper ).css( 'height', `${ ppcpStyle.height }px` );
}
const button = this.paymentsClient.createButton( {
onClick: this.onButtonClick.bind( this ),
allowedPaymentMethods: [ baseCardPaymentMethod ],
buttonColor: buttonStyle.color || 'black',
buttonType: buttonStyle.type || 'pay',
buttonLocale: buttonStyle.language || 'en',
buttonSizeMode: 'fill',
} );
jQuery( wrapper ).append( button );
} );
}
addButtonCheckout( baseCardPaymentMethod, wrapper, buttonStyle ) {
/**
* @see https://developers.google.com/pay/api/web/reference/client#createButton
*/
const button = this.paymentsClient.createButton( {
onClick: this.onButtonClick.bind( this ),
onClick: this.onButtonClick,
allowedPaymentMethods: [ baseCardPaymentMethod ],
buttonColor: buttonStyle.color || 'black',
buttonType: buttonStyle.type || 'pay',
buttonLocale: buttonStyle.language || 'en',
buttonColor: color || 'black',
buttonType: type || 'pay',
buttonLocale: language || 'en',
buttonSizeMode: 'fill',
} );
wrapper.appendChild( button );
}
waitForWrapper( selector, callback, delay = 100, timeout = 2000 ) {
const startTime = Date.now();
const interval = setInterval( () => {
const el = document.querySelector( selector );
const timeElapsed = Date.now() - startTime;
if ( el ) {
clearInterval( interval );
callback( el );
} else if ( timeElapsed > timeout ) {
clearInterval( interval );
}
}, delay );
this.insertButton( button );
}
//------------------------
@ -290,35 +383,49 @@ class GooglepayButton {
* Show Google Pay payment sheet when Google Pay payment button is clicked
*/
onButtonClick() {
this.log( 'onButtonClick', this.context );
this.log( 'onButtonClick' );
const initiatePaymentRequest = () => {
window.ppcpFundingSource = 'googlepay';
const paymentDataRequest = this.paymentDataRequest();
this.log(
'onButtonClick: paymentDataRequest',
paymentDataRequest,
this.context
);
this.paymentsClient.loadPaymentData( paymentDataRequest );
};
if ( 'function' === typeof this.contextHandler.validateForm ) {
// During regular checkout, validate the checkout form before initiating the payment.
this.contextHandler
.validateForm()
.then( initiatePaymentRequest, () => {
console.error(
'[GooglePayButton] Form validation failed.'
);
const validateForm = () => {
if ( 'function' !== typeof this.contextHandler.validateForm ) {
return Promise.resolve();
}
return this.contextHandler.validateForm().catch( ( error ) => {
this.error( 'Form validation failed:', error );
throw error;
} );
};
const getTransactionInfo = () => {
if ( 'function' !== typeof this.contextHandler.transactionInfo ) {
return Promise.resolve();
}
return this.contextHandler
.transactionInfo()
.then( ( transactionInfo ) => {
this.transactionInfo = transactionInfo;
} )
.catch( ( error ) => {
this.error( 'Failed to get transaction info:', error );
throw error;
} );
} else {
// This is the flow on product page, cart, and other non-checkout pages.
initiatePaymentRequest();
}
};
validateForm()
.then( getTransactionInfo )
.then( initiatePaymentRequest );
}
paymentDataRequest() {
@ -334,10 +441,7 @@ class GooglepayButton {
paymentDataRequest.transactionInfo = this.transactionInfo;
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
if (
this.buttonConfig.shipping.enabled &&
this.contextHandler.shippingAllowed()
) {
if ( this.requiresShipping ) {
paymentDataRequest.callbackIntents = [
'SHIPPING_ADDRESS',
'SHIPPING_OPTION',
@ -366,8 +470,7 @@ class GooglepayButton {
}
onPaymentDataChanged( paymentData ) {
this.log( 'onPaymentDataChanged', this.context );
this.log( 'paymentData', paymentData );
this.log( 'onPaymentDataChanged', paymentData );
return new Promise( async ( resolve, reject ) => {
try {
@ -412,7 +515,7 @@ class GooglepayButton {
resolve( paymentDataRequestUpdate );
} catch ( error ) {
console.error( 'Error during onPaymentDataChanged:', error );
this.error( 'Error during onPaymentDataChanged:', error );
reject( error );
}
} );
@ -440,18 +543,18 @@ class GooglepayButton {
//------------------------
onPaymentAuthorized( paymentData ) {
this.log( 'onPaymentAuthorized', this.context );
this.log( 'onPaymentAuthorized' );
return this.processPayment( paymentData );
}
async processPayment( paymentData ) {
this.log( 'processPayment', this.context );
this.log( 'processPayment' );
return new Promise( async ( resolve, reject ) => {
try {
const id = await this.contextHandler.createOrder();
this.log( 'processPayment: createOrder', id, this.context );
this.log( 'processPayment: createOrder', id );
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
@ -462,8 +565,7 @@ class GooglepayButton {
this.log(
'processPayment: confirmOrder',
confirmOrderResponse,
this.context
confirmOrderResponse
);
/** Capture the Order on the Server */
@ -533,7 +635,7 @@ class GooglepayButton {
};
}
this.log( 'processPaymentResponse', response, this.context );
this.log( 'processPaymentResponse', response );
return response;
}

View file

@ -20,7 +20,7 @@ class GooglepayManager {
bootstrap.handler
);
const button = new GooglepayButton(
const button = GooglepayButton.createButton(
bootstrap.context,
bootstrap.handler,
buttonConfig,
@ -30,13 +30,19 @@ class GooglepayManager {
this.buttons.push( button );
const initButton = () => {
button.configure( this.googlePayConfig, this.transactionInfo );
button.init();
};
// Initialize button only if googlePayConfig and transactionInfo are already fetched.
if ( this.googlePayConfig && this.transactionInfo ) {
button.init( this.googlePayConfig, this.transactionInfo );
initButton();
} else {
await this.init();
if ( this.googlePayConfig && this.transactionInfo ) {
button.init( this.googlePayConfig, this.transactionInfo );
initButton();
}
}
} );
@ -53,8 +59,18 @@ class GooglepayManager {
this.transactionInfo = await this.fetchTransactionInfo();
}
for ( const button of this.buttons ) {
button.init( this.googlePayConfig, this.transactionInfo );
if ( ! this.googlePayConfig ) {
console.error( 'No GooglePayConfig received during init' );
} else if ( ! this.transactionInfo ) {
console.error( 'No transactionInfo found during init' );
} else {
for ( const button of this.buttons ) {
button.configure(
this.googlePayConfig,
this.transactionInfo
);
button.init();
}
}
} catch ( error ) {
console.error( 'Error during initialization:', error );

View file

@ -0,0 +1,78 @@
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
import GooglepayButton from './GooglepayButton';
/**
* A single GooglePay preview button instance.
*/
export default class GooglePayPreviewButton extends PreviewButton {
/**
* Instance of the preview button.
*
* @type {?PaymentButton}
*/
#button = null;
constructor( args ) {
super( args );
this.selector = `${ args.selector }GooglePay`;
this.defaultAttributes = {
button: {
style: {
type: 'pay',
color: 'black',
language: 'en',
},
},
};
}
createNewWrapper() {
const element = super.createNewWrapper();
element.addClass( 'ppcp-button-googlepay' );
return element;
}
createButton( buttonConfig ) {
const contextHandler = ContextHandlerFactory.create(
'preview',
buttonConfig,
this.ppcpConfig,
null
);
if ( ! this.#button ) {
/* Intentionally using `new` keyword, instead of the `.createButton()` factory,
* as the factory is designed to only create a single button per context, while a single
* page can contain multiple instances of a preview button.
*/
this.#button = new GooglepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig,
contextHandler
);
}
this.#button.configure( this.apiConfig, null );
this.#button.applyButtonStyles( buttonConfig, this.ppcpConfig );
this.#button.reinit();
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
*
* @param {Object} buttonConfig
* @param {Object} ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button && buttonConfig.button ) {
Object.assign( buttonConfig.button.style, ppcpConfig.button.style );
}
}
}

View file

@ -1,7 +1,5 @@
import GooglepayButton from './GooglepayButton';
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
import GooglePayPreviewButton from './GooglepayPreviewButton';
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
@ -33,7 +31,7 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager {
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
* @return {Promise<{}>} Promise that resolves when API configuration is available.
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Googlepay()?.config;
@ -59,7 +57,7 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager {
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {GooglePayPreviewButton}
* @return {GooglePayPreviewButton} The new preview button instance.
*/
createButtonInstance( wrapperId ) {
return new GooglePayPreviewButton( {
@ -69,64 +67,5 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager {
}
}
/**
* A single GooglePay preview button instance.
*/
class GooglePayPreviewButton extends PreviewButton {
constructor( args ) {
super( args );
this.selector = `${ args.selector }GooglePay`;
this.defaultAttributes = {
button: {
style: {
type: 'pay',
color: 'black',
language: 'en',
},
},
};
}
createNewWrapper() {
const element = super.createNewWrapper();
element.addClass( 'ppcp-button-googlepay' );
return element;
}
createButton( buttonConfig ) {
const contextHandler = ContextHandlerFactory.create(
'preview',
buttonConfig,
this.ppcpConfig,
null
);
const button = new GooglepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig,
contextHandler
);
button.init( this.apiConfig, null );
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
* @param buttonConfig
* @param ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button && buttonConfig.button ) {
Object.assign( buttonConfig.button.style, ppcpConfig.button.style );
}
}
}
// Initialize the preview button manager.
buttonManager();

View file

@ -290,6 +290,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
$this->hide_gateway_until_eligible();
},
21
);
@ -303,6 +304,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
$this->hide_gateway_until_eligible();
},
21
);
@ -335,6 +337,23 @@ class Button implements ButtonInterface {
<?php
}
/**
* Outputs an inline CSS style that hides the Google Pay gateway (on Classic Checkout).
* The style is removed by `PaymentButton.js` once the eligibility of the payment method
* is confirmed.
*
* @return void
*/
protected function hide_gateway_until_eligible() : void {
?>
<style data-hide-gateway='<?php echo esc_attr( GooglePayGateway::ID ); ?>'>
.wc_payment_method.payment_method_ppcp-googlepay {
display: none;
}
</style>
<?php
}
/**
* Enqueues scripts/styles.
*/

View file

@ -92,6 +92,11 @@ class GooglePayGateway extends WC_Payment_Gateway {
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'Google Pay (via PayPal) ', 'woocommerce-paypal-payments' );
$this->method_description = __( 'The separate payment gateway with the Google Pay button. If disabled, the button is included in the PayPal gateway.', 'woocommerce-paypal-payments' );

View file

@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.25.0"
}
],
[
"@babel/preset-react"
]
]
}

View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-local-alternative-payment-methods",
"type": "dhii-mod",
"description": "Country based Alternative Payment Methods module",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\LocalAlternativePaymentMethods\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,12 @@
<?php
/**
* The local alternative payment methods module extensions.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
return array();

View file

@ -0,0 +1,16 @@
<?php
/**
* The local alternative payment methods module.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new LocalAlternativePaymentMethodsModule();
};

View file

@ -0,0 +1,33 @@
{
"name": "ppcp-local-alternative-payment-methods",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,9 @@
export function APM( { config, components } ) {
const { PaymentMethodIcons } = components;
return (
<div>
<PaymentMethodIcons icons={ [ config.icon ] } align="right" />
</div>
);
}

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-bancontact_data' );
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <APM config={ config } />,
edit: <div></div>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
features: config.supports,
},
} );

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-blik_data' );
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <APM config={ config } />,
edit: <div></div>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
features: config.supports,
},
} );

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-eps_data' );
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <APM config={ config } />,
edit: <div></div>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
features: config.supports,
},
} );

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-ideal_data' );
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <APM config={ config } />,
edit: <div></div>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
features: config.supports,
},
} );

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-mybank_data' );
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <APM config={ config } />,
edit: <div></div>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
features: config.supports,
},
} );

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-p24_data' );
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <APM config={ config } />,
edit: <div></div>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
features: config.supports,
},
} );

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-trustly_data' );
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <APM config={ config } />,
edit: <div></div>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
features: config.supports,
},
} );

View file

@ -0,0 +1,170 @@
<?php
/**
* The local alternative payment methods module services.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'ppcp-local-apms.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-local-alternative-payment-methods/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'ppcp-local-apms.payment-methods' => static function( ContainerInterface $container ): array {
return array(
'bancontact' => array(
'id' => BancontactGateway::ID,
'countries' => array( 'BE' ),
'currencies' => array( 'EUR' ),
),
'blik' => array(
'id' => BlikGateway::ID,
'countries' => array( 'PL' ),
'currencies' => array( 'PLN' ),
),
'eps' => array(
'id' => EPSGateway::ID,
'countries' => array( 'AT' ),
'currencies' => array( 'EUR' ),
),
'ideal' => array(
'id' => IDealGateway::ID,
'countries' => array( 'NL' ),
'currencies' => array( 'EUR' ),
),
'mybank' => array(
'id' => MyBankGateway::ID,
'countries' => array( 'IT' ),
'currencies' => array( 'EUR' ),
),
'p24' => array(
'id' => P24Gateway::ID,
'countries' => array( 'PL' ),
'currencies' => array( 'EUR', 'PLN' ),
),
'trustly' => array(
'id' => TrustlyGateway::ID,
'countries' => array( 'AT', 'DE', 'DK', 'EE', 'ES', 'FI', 'GB', 'LT', 'LV', 'NL', 'NO', 'SE' ),
'currencies' => array( 'EUR', 'DKK', 'SEK', 'GBP', 'NOK' ),
),
);
},
'ppcp-local-apms.bancontact.wc-gateway' => static function ( ContainerInterface $container ): BancontactGateway {
return new BancontactGateway(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.blik.wc-gateway' => static function ( ContainerInterface $container ): BlikGateway {
return new BlikGateway(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.eps.wc-gateway' => static function ( ContainerInterface $container ): EPSGateway {
return new EPSGateway(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.ideal.wc-gateway' => static function ( ContainerInterface $container ): IDealGateway {
return new IDealGateway(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.mybank.wc-gateway' => static function ( ContainerInterface $container ): MyBankGateway {
return new MyBankGateway(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.p24.wc-gateway' => static function ( ContainerInterface $container ): P24Gateway {
return new P24Gateway(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.trustly.wc-gateway' => static function ( ContainerInterface $container ): TrustlyGateway {
return new TrustlyGateway(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.bancontact.payment-method' => static function( ContainerInterface $container ): BancontactPaymentMethod {
return new BancontactPaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.bancontact.wc-gateway' )
);
},
'ppcp-local-apms.blik.payment-method' => static function( ContainerInterface $container ): BlikPaymentMethod {
return new BlikPaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.blik.wc-gateway' )
);
},
'ppcp-local-apms.eps.payment-method' => static function( ContainerInterface $container ): EPSPaymentMethod {
return new EPSPaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.eps.wc-gateway' )
);
},
'ppcp-local-apms.ideal.payment-method' => static function( ContainerInterface $container ): IDealPaymentMethod {
return new IDealPaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.ideal.wc-gateway' )
);
},
'ppcp-local-apms.mybank.payment-method' => static function( ContainerInterface $container ): MyBankPaymentMethod {
return new MyBankPaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.mybank.wc-gateway' )
);
},
'ppcp-local-apms.p24.payment-method' => static function( ContainerInterface $container ): P24PaymentMethod {
return new P24PaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.p24.wc-gateway' )
);
},
'ppcp-local-apms.trustly.payment-method' => static function( ContainerInterface $container ): TrustlyPaymentMethod {
return new TrustlyPaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.trustly.wc-gateway' )
);
},
);

View file

@ -0,0 +1,226 @@
<?php
/**
* The Bancontact payment gateway.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class BancontactGateway
*/
class BancontactGateway extends WC_Payment_Gateway {
const ID = 'ppcp-bancontact';
/**
* PayPal Orders endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* Purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* BancontactGateway constructor.
*
* @param Orders $orders_endpoint PayPal Orders endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory Purchase unit factory.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
*/
public function __construct(
Orders $orders_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'Bancontact', 'woocommerce-paypal-payments' );
$this->method_description = __( 'A popular and trusted electronic payment method in Belgium, used by Belgian customers with Bancontact cards issued by local banks. Transactions are processed in EUR.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'Bancontact', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_bancontact_color.svg' );
$this->init_form_fields();
$this->init_settings();
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
$this->orders_endpoint = $orders_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Bancontact', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable Bancontact payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$wc_order->update_status( 'on-hold', __( 'Awaiting Bancontact to confirm the payment.', 'woocommerce-paypal-payments' ) );
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$amount = $purchase_unit->amount()->to_array();
$request_body = array(
'intent' => 'CAPTURE',
'payment_source' => array(
'bancontact' => array(
'country_code' => $wc_order->get_billing_country(),
'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(),
),
),
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array(
array(
'reference_id' => $purchase_unit->reference_id(),
'amount' => array(
'currency_code' => $amount['currency_code'],
'value' => $amount['value'],
),
'custom_id' => $purchase_unit->custom_id(),
'invoice_id' => $purchase_unit->invoice_id(),
),
),
'application_context' => array(
'locale' => 'en-BE',
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
$body = json_decode( $response['body'] );
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* Bancontact payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class BancontactPaymentMethod
*/
class BancontactPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* Bancontact WC gateway.
*
* @var BancontactGateway
*/
private $gateway;
/**
* BancontactPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param BancontactGateway $gateway Bancontact WC gateway.
*/
public function __construct(
string $module_url,
string $version,
BancontactGateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = BancontactGateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-bancontact-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/bancontact-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-bancontact-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_bancontact_color.svg' ),
);
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* The Blik payment gateway.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class BlikGateway
*/
class BlikGateway extends WC_Payment_Gateway {
const ID = 'ppcp-blik';
/**
* PayPal Orders endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* Purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* BlikGateway constructor.
*
* @param Orders $orders_endpoint PayPal Orders endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory Purchase unit factory.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
*/
public function __construct(
Orders $orders_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'Blik', 'woocommerce-paypal-payments' );
$this->method_description = __( 'A widely used mobile payment method in Poland, allowing Polish customers to pay directly via their banking apps. Transactions are processed in PLN.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'Blik', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_blik_color.svg' );
$this->init_form_fields();
$this->init_settings();
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
$this->orders_endpoint = $orders_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Blik', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable Blik payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$wc_order->update_status( 'on-hold', __( 'Awaiting Blik to confirm the payment.', 'woocommerce-paypal-payments' ) );
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$amount = $purchase_unit->amount()->to_array();
$request_body = array(
'intent' => 'CAPTURE',
'payment_source' => array(
'blik' => array(
'country_code' => $wc_order->get_billing_country(),
'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(),
'email' => $wc_order->get_billing_email(),
),
),
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array(
array(
'reference_id' => $purchase_unit->reference_id(),
'amount' => array(
'currency_code' => $amount['currency_code'],
'value' => $amount['value'],
),
'custom_id' => $purchase_unit->custom_id(),
'invoice_id' => $purchase_unit->invoice_id(),
),
),
'application_context' => array(
'locale' => 'en-PL',
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
$body = json_decode( $response['body'] );
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* Blik payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class BlikPaymentMethod
*/
class BlikPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* Blik WC gateway.
*
* @var BlikGateway
*/
private $gateway;
/**
* BlikPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param BlikGateway $gateway Blik WC gateway.
*/
public function __construct(
string $module_url,
string $version,
BlikGateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = BlikGateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-blick-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/blik-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-blick-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_blik_color.svg' ),
);
}
}

View file

@ -0,0 +1,226 @@
<?php
/**
* The EPS payment gateway.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class EPSGateway
*/
class EPSGateway extends WC_Payment_Gateway {
const ID = 'ppcp-eps';
/**
* PayPal Orders endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* Purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* EPSGateway constructor.
*
* @param Orders $orders_endpoint PayPal Orders endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory Purchase unit factory.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
*/
public function __construct(
Orders $orders_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'EPS', 'woocommerce-paypal-payments' );
$this->method_description = __( 'An online payment method in Austria, enabling Austrian buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'EPS', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_eps_color.svg' );
$this->init_form_fields();
$this->init_settings();
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
$this->orders_endpoint = $orders_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'EPS', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable EPS payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$wc_order->update_status( 'on-hold', __( 'Awaiting EPS to confirm the payment.', 'woocommerce-paypal-payments' ) );
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$amount = $purchase_unit->amount()->to_array();
$request_body = array(
'intent' => 'CAPTURE',
'payment_source' => array(
'eps' => array(
'country_code' => $wc_order->get_billing_country(),
'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(),
),
),
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array(
array(
'reference_id' => $purchase_unit->reference_id(),
'amount' => array(
'currency_code' => $amount['currency_code'],
'value' => $amount['value'],
),
'custom_id' => $purchase_unit->custom_id(),
'invoice_id' => $purchase_unit->invoice_id(),
),
),
'application_context' => array(
'locale' => 'en-AT',
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
$body = json_decode( $response['body'] );
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* EPS payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class EPSPaymentMethod
*/
class EPSPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* EPS WC gateway.
*
* @var EPSGateway
*/
private $gateway;
/**
* EPSPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param EPSGateway $gateway EPS WC gateway.
*/
public function __construct(
string $module_url,
string $version,
EPSGateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = EPSGateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-eps-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/eps-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-eps-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_eps_color.svg' ),
);
}
}

View file

@ -0,0 +1,228 @@
<?php
/**
* The iDeal payment gateway.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class IDealGateway
*/
class IDealGateway extends WC_Payment_Gateway {
const ID = 'ppcp-ideal';
/**
* PayPal Orders endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* Purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* IDealGateway constructor.
*
* @param Orders $orders_endpoint PayPal Orders endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory Purchase unit factory.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
*/
public function __construct(
Orders $orders_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'iDeal', 'woocommerce-paypal-payments' );
$this->method_description = __( 'The most common payment method in the Netherlands, allowing Dutch buyers to pay directly through their preferred bank. Transactions are processed in EUR.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'iDeal', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_ideal_color.svg' );
$this->init_form_fields();
$this->init_settings();
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
$this->orders_endpoint = $orders_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'iDeal', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable iDeal payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$wc_order->update_status( 'on-hold', __( 'Awaiting iDeal to confirm the payment.', 'woocommerce-paypal-payments' ) );
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$amount = $purchase_unit->amount()->to_array();
$payment_source = array(
'country_code' => $wc_order->get_billing_country(),
'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(),
);
// TODO get "bic" from gateway settings.
$request_body = array(
'intent' => 'CAPTURE',
'payment_source' => array(
'ideal' => $payment_source,
),
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array(
array(
'reference_id' => $purchase_unit->reference_id(),
'amount' => array(
'currency_code' => $amount['currency_code'],
'value' => $amount['value'],
),
'custom_id' => $purchase_unit->custom_id(),
'invoice_id' => $purchase_unit->invoice_id(),
),
),
'application_context' => array(
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
$body = json_decode( $response['body'] );
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* IDeal payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class IDealPaymentMethod
*/
class IDealPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* IDeal WC gateway.
*
* @var IDealGateway
*/
private $gateway;
/**
* IDealPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param IDealGateway $gateway IDeal WC gateway.
*/
public function __construct(
string $module_url,
string $version,
IDealGateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = IDealGateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-ideal-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/ideal-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-ideal-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_ideal_color.svg' ),
);
}
}

View file

@ -0,0 +1,171 @@
<?php
/**
* The local alternative payment methods module.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WC_Order;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class LocalAlternativePaymentMethodsModule
*/
class LocalAlternativePaymentMethodsModule implements ModuleInterface {
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
add_filter(
'woocommerce_payment_gateways',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ) {
if ( ! is_array( $methods ) ) {
return $methods;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $payment_methods as $key => $value ) {
$methods[] = $c->get( 'ppcp-local-apms.' . $key . '.wc-gateway' );
}
return $methods;
}
);
add_filter(
'woocommerce_available_payment_gateways',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ) {
if ( ! is_array( $methods ) ) {
return $methods;
}
if ( ! is_admin() ) {
if ( ! isset( WC()->customer ) ) {
return $methods;
}
$customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country();
$site_currency = get_woocommerce_currency();
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $payment_methods as $payment_method ) {
if (
! in_array( $customer_country, $payment_method['countries'], true )
|| ! in_array( $site_currency, $payment_method['currencies'], true )
) {
unset( $methods[ $payment_method['id'] ] );
}
}
}
return $methods;
}
);
add_action(
'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $payment_methods as $key => $value ) {
$payment_method_registry->register( $c->get( 'ppcp-local-apms.' . $key . '.payment-method' ) );
}
}
);
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function ( array $data ) use ( $c ) {
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
$default_disable_funding = $data['url_params']['disable-funding'] ?? '';
$disable_funding = array_merge( array_keys( $payment_methods ), array_filter( explode( ',', $default_disable_funding ) ) );
$data['url_params']['disable-funding'] = implode( ',', array_unique( $disable_funding ) );
return $data;
}
);
add_action(
'woocommerce_before_thankyou',
/**
* Activate is_checkout() on woocommerce/classic-shortcode checkout blocks.
*
* @psalm-suppress MissingClosureParamType
*/
function( $order_id ) use ( $c ) {
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$cancelled = wc_clean( wp_unslash( $_GET['cancelled'] ?? '' ) );
$order_key = wc_clean( wp_unslash( $_GET['key'] ?? '' ) );
// phpcs:enable
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
if (
! $this->is_local_apm( $order->get_payment_method(), $payment_methods )
|| ! $cancelled
|| $order->get_order_key() !== $order_key
) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$error_code = wc_clean( wp_unslash( $_GET['errorcode'] ?? '' ) );
if ( $error_code === 'processing_error' || $error_code === 'payment_error' ) {
$order->update_status( 'failed', __( "The payment can't be processed because of an error.", 'woocommerce-paypal-payments' ) );
add_filter( 'woocommerce_order_has_status', '__return_true' );
}
}
);
}
/**
* Check if given payment method is a local APM.
*
* @param string $selected_payment_method Selected payment method.
* @param array $payment_methods Available local APMs.
* @return bool
*/
private function is_local_apm( string $selected_payment_method, array $payment_methods ): bool {
foreach ( $payment_methods as $payment_method ) {
if ( $payment_method['id'] === $selected_payment_method ) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,226 @@
<?php
/**
* The MyBank payment gateway.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class MyBankGateway
*/
class MyBankGateway extends WC_Payment_Gateway {
const ID = 'ppcp-mybank';
/**
* PayPal Orders endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* Purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* MyBankGateway constructor.
*
* @param Orders $orders_endpoint PayPal Orders endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory Purchase unit factory.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
*/
public function __construct(
Orders $orders_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'MyBank', 'woocommerce-paypal-payments' );
$this->method_description = __( 'A European online banking payment solution primarily used in Italy, enabling customers to make secure bank transfers during checkout. Transactions are processed in EUR.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'MyBank', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_mybank_color.svg' );
$this->init_form_fields();
$this->init_settings();
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
$this->orders_endpoint = $orders_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'MyBank', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable MyBank payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$wc_order->update_status( 'on-hold', __( 'Awaiting MyBank to confirm the payment.', 'woocommerce-paypal-payments' ) );
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$amount = $purchase_unit->amount()->to_array();
$request_body = array(
'intent' => 'CAPTURE',
'payment_source' => array(
'mybank' => array(
'country_code' => $wc_order->get_billing_country(),
'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(),
),
),
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array(
array(
'reference_id' => $purchase_unit->reference_id(),
'amount' => array(
'currency_code' => $amount['currency_code'],
'value' => $amount['value'],
),
'custom_id' => $purchase_unit->custom_id(),
'invoice_id' => $purchase_unit->invoice_id(),
),
),
'application_context' => array(
'locale' => 'en-IT',
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
$body = json_decode( $response['body'] );
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* MyBank payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class MyBankPaymentMethod
*/
class MyBankPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* MyBank WC gateway.
*
* @var MyBankGateway
*/
private $gateway;
/**
* MyBankPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param MyBankGateway $gateway MyBank WC gateway.
*/
public function __construct(
string $module_url,
string $version,
MyBankGateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = MyBankGateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-mybank-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/mybank-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-mybank-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_mybank_color.svg' ),
);
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* The P24 payment gateway.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class P24Gateway
*/
class P24Gateway extends WC_Payment_Gateway {
const ID = 'ppcp-p24';
/**
* PayPal Orders endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* Purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* P24Gateway constructor.
*
* @param Orders $orders_endpoint PayPal Orders endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory Purchase unit factory.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
*/
public function __construct(
Orders $orders_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'Przelewy24', 'woocommerce-paypal-payments' );
$this->method_description = __( 'A popular online payment gateway in Poland, offering various payment options for Polish customers. Transactions can be processed in PLN or EUR.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'Przelewy24', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_przelewy24_color.svg' );
$this->init_form_fields();
$this->init_settings();
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
$this->orders_endpoint = $orders_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Przelewy24', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable Przelewy24 payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$wc_order->update_status( 'on-hold', __( 'Awaiting Przelewy24 to confirm the payment.', 'woocommerce-paypal-payments' ) );
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$amount = $purchase_unit->amount()->to_array();
$request_body = array(
'intent' => 'CAPTURE',
'payment_source' => array(
'p24' => array(
'country_code' => $wc_order->get_billing_country(),
'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(),
'email' => $wc_order->get_billing_email(),
),
),
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array(
array(
'reference_id' => $purchase_unit->reference_id(),
'amount' => array(
'currency_code' => $amount['currency_code'],
'value' => $amount['value'],
),
'custom_id' => $purchase_unit->custom_id(),
'invoice_id' => $purchase_unit->invoice_id(),
),
),
'application_context' => array(
'locale' => 'en-PL',
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
$body = json_decode( $response['body'] );
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* Przelewy24 payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class P24PaymentMethod
*/
class P24PaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* P24Gateway WC gateway.
*
* @var P24Gateway
*/
private $gateway;
/**
* P24PaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param P24Gateway $gateway Przelewy24 WC gateway.
*/
public function __construct(
string $module_url,
string $version,
P24Gateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = P24Gateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-p24-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/p24-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-p24-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_przelewy24_color.svg' ),
);
}
}

View file

@ -0,0 +1,249 @@
<?php
/**
* The Trustly payment gateway.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class TrustlyGateway
*/
class TrustlyGateway extends WC_Payment_Gateway {
const ID = 'ppcp-trustly';
/**
* PayPal Orders endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* Purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* Service able to provide transaction url for an order.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* TrustlyGateway constructor.
*
* @param Orders $orders_endpoint PayPal Orders endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory Purchase unit factory.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
*/
public function __construct(
Orders $orders_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider
) {
$this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'Trustly', 'woocommerce-paypal-payments' );
$this->method_description = __( 'A European payment method that allows buyers to make payments directly from their bank accounts, suitable for customers across multiple European countries. Supported currencies include EUR, DKK, SEK, GBP, and NOK.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'Trustly', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_trustly_color.svg' );
$this->init_form_fields();
$this->init_settings();
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
$this->orders_endpoint = $orders_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Trustly', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable Trustly payment gateway.', 'woocommerce-paypal-payments' ),
),
'title' => array(
'title' => __( 'Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->title,
'desc_tip' => true,
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
'description' => array(
'title' => __( 'Description', 'woocommerce-paypal-payments' ),
'type' => 'text',
'default' => $this->description,
'desc_tip' => true,
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ),
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$wc_order->update_status( 'on-hold', __( 'Awaiting Trustly to confirm the payment.', 'woocommerce-paypal-payments' ) );
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$amount = $purchase_unit->amount()->to_array();
$request_body = array(
'intent' => 'CAPTURE',
'payment_source' => array(
'trustly' => array(
'country_code' => $wc_order->get_billing_country(),
'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(),
),
),
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array(
array(
'reference_id' => $purchase_unit->reference_id(),
'amount' => array(
'currency_code' => $amount['currency_code'],
'value' => $amount['value'],
),
'custom_id' => $purchase_unit->custom_id(),
'invoice_id' => $purchase_unit->invoice_id(),
),
),
'application_context' => array(
'locale' => $this->valid_bcp47_code(),
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
$body = json_decode( $response['body'] );
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
}
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*
* @param \WC_Order $order WC order to get transaction url by.
*
* @return string
*/
public function get_transaction_url( $order ): string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
}
/**
* Returns a PayPal-supported BCP-47 code, for example de-DE-formal becomes de-DE.
*
* @return string
*/
private function valid_bcp47_code() {
$locale = str_replace( '_', '-', get_user_locale() );
if ( preg_match( '/^[a-z]{2}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}))?$/', $locale ) ) {
return $locale;
}
$parts = explode( '-', $locale );
if ( count( $parts ) === 3 ) {
$ret = substr( $locale, 0, strrpos( $locale, '-' ) );
if ( false !== $ret ) {
return $ret;
}
}
return 'en';
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* Trustly payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class TrustlyPaymentMethod
*/
class TrustlyPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* TrustlyGateway WC gateway.
*
* @var TrustlyGateway
*/
private $gateway;
/**
* TrustlyPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param TrustlyGateway $gateway Trustly WC gateway.
*/
public function __construct(
string $module_url,
string $version,
TrustlyGateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = TrustlyGateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-trustly-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/trustly-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-trustly-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_trustly_color.svg' ),
);
}
}

View file

@ -0,0 +1,60 @@
const path = require( 'path' );
const isProduction = process.env.NODE_ENV === 'production';
const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'bancontact-payment-method': path.resolve(
'./resources/js/bancontact-payment-method.js'
),
'blik-payment-method': path.resolve(
'./resources/js/blik-payment-method.js'
),
'eps-payment-method': path.resolve(
'./resources/js/eps-payment-method.js'
),
'ideal-payment-method': path.resolve(
'./resources/js/ideal-payment-method.js'
),
'mybank-payment-method': path.resolve(
'./resources/js/mybank-payment-method.js'
),
'p24-payment-method': path.resolve(
'./resources/js/p24-payment-method.js'
),
'trustly-payment-method': path.resolve(
'./resources/js/trustly-payment-method.js'
),
},
output: {
path: path.resolve( __dirname, 'assets/' ),
filename: 'js/[name].js',
},
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
},
},
{ loader: 'sass-loader' },
],
},
],
},
};

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking;
use WooCommerce\PayPalCommerce\OrderTracking\Integration\DhlShipmentIntegration;
use WooCommerce\PayPalCommerce\OrderTracking\Integration\GermanizedShipmentIntegration;
use WooCommerce\PayPalCommerce\OrderTracking\Integration\ShipmentTrackingIntegration;
use WooCommerce\PayPalCommerce\OrderTracking\Integration\ShipStationIntegration;
@ -118,6 +119,7 @@ return array(
$is_gzd_active = $container->get( 'compat.gzd.is_supported_plugin_version_active' );
$is_wc_shipment_active = $container->get( 'compat.wc_shipment_tracking.is_supported_plugin_version_active' );
$is_yith_ywot_active = $container->get( 'compat.ywot.is_supported_plugin_version_active' );
$is_dhl_de_active = $container->get( 'compat.dhl.is_supported_plugin_version_active' );
$is_ship_station_active = $container->get( 'compat.shipstation.is_supported_plugin_version_active' );
$is_wc_shipping_tax_active = $container->get( 'compat.wc_shipping_tax.is_supported_plugin_version_active' );
@ -135,6 +137,10 @@ return array(
$integrations[] = new YithShipmentIntegration( $shipment_factory, $logger, $endpoint );
}
if ( $is_dhl_de_active ) {
$integrations[] = new DhlShipmentIntegration( $shipment_factory, $logger, $endpoint );
}
if ( $is_ship_station_active ) {
$integrations[] = new ShipStationIntegration( $shipment_factory, $logger, $endpoint );
}

View file

@ -0,0 +1,112 @@
<?php
/**
* The Shipment integration for DHL Shipping Germany for WooCommerce plugin.
*
* @package WooCommerce\PayPalCommerce\OrderTracking\Integration
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking\Integration;
use Psr\Log\LoggerInterface;
use WC_Order;
use Exception;
use WooCommerce\PayPalCommerce\Compat\Integration;
use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactoryInterface;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use function WooCommerce\PayPalCommerce\Api\ppcp_get_paypal_order;
/**
* Class DhlShipmentIntegration
*/
class DhlShipmentIntegration implements Integration {
use TransactionIdHandlingTrait;
/**
* The shipment factory.
*
* @var ShipmentFactoryInterface
*/
protected $shipment_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* The order tracking endpoint.
*
* @var OrderTrackingEndpoint
*/
protected $endpoint;
/**
* The DhlShipmentIntegration constructor.
*
* @param ShipmentFactoryInterface $shipment_factory The shipment factory.
* @param LoggerInterface $logger The logger.
* @param OrderTrackingEndpoint $endpoint The order tracking endpoint.
*/
public function __construct(
ShipmentFactoryInterface $shipment_factory,
LoggerInterface $logger,
OrderTrackingEndpoint $endpoint
) {
$this->shipment_factory = $shipment_factory;
$this->logger = $logger;
$this->endpoint = $endpoint;
}
/**
* {@inheritDoc}
*/
public function integrate(): void {
add_action(
'pr_save_dhl_label_tracking',
function( int $order_id, array $tracking_details ) {
try {
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return;
}
$paypal_order = ppcp_get_paypal_order( $wc_order );
$capture_id = $this->get_paypal_order_transaction_id( $paypal_order );
$tracking_number = $tracking_details['tracking_number'];
$carrier = $tracking_details['carrier'];
if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $capture_id ) {
return;
}
$ppcp_shipment = $this->shipment_factory->create_shipment(
$order_id,
$capture_id,
$tracking_number,
'SHIPPED',
'DE_DHL',
$carrier,
array()
);
$tracking_information = $this->endpoint->get_tracking_information( $order_id, $tracking_number );
$tracking_information
? $this->endpoint->update_tracking_information( $ppcp_shipment, $order_id )
: $this->endpoint->add_tracking_information( $ppcp_shipment, $order_id );
} catch ( Exception $exception ) {
return;
}
},
600,
2
);
}
}

View file

@ -0,0 +1,230 @@
import {
getCurrentPaymentMethod,
PaymentMethods,
} from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
export function buttonConfiguration( ppcp_add_payment_method, errorHandler ) {
return {
createVaultSetupToken: async () => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_setup_token.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax.create_setup_token
.nonce,
} ),
}
);
const result = await response.json();
if ( result.data.id ) {
return result.data.id;
}
errorHandler.message( ppcp_add_payment_method.error_message );
},
onApprove: async ( { vaultSetupToken } ) => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_payment_token.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax.create_payment_token
.nonce,
vault_setup_token: vaultSetupToken,
} ),
}
);
const result = await response.json();
if ( result.success === true ) {
window.location.href =
ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message( ppcp_add_payment_method.error_message );
},
onError: ( error ) => {
console.error( error );
errorHandler.message( ppcp_add_payment_method.error_message );
},
};
}
export function cardFieldsConfiguration(
ppcp_add_payment_method,
errorHandler
) {
return {
createVaultSetupToken: async () => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_setup_token.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax.create_setup_token
.nonce,
payment_method: PaymentMethods.CARDS,
verification_method:
ppcp_add_payment_method.verification_method,
} ),
}
);
const result = await response.json();
if ( result.data.id ) {
return result.data.id;
}
errorHandler.message( ppcp_add_payment_method.error_message );
},
onApprove: async ( { vaultSetupToken } ) => {
const isFreeTrialCart =
ppcp_add_payment_method?.is_free_trial_cart ?? false;
const response = await fetch(
ppcp_add_payment_method.ajax.create_payment_token.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax.create_payment_token
.nonce,
vault_setup_token: vaultSetupToken,
payment_method: PaymentMethods.CARDS,
is_free_trial_cart: isFreeTrialCart,
} ),
}
);
const result = await response.json();
if ( result.success === true ) {
const context = ppcp_add_payment_method?.context ?? '';
if ( context === 'checkout' ) {
document.querySelector( '#place_order' ).click();
return;
}
if (
ppcp_add_payment_method.is_subscription_change_payment_page
) {
const subscriptionId =
ppcp_add_payment_method.subscription_id_to_change_payment;
if ( subscriptionId && result.data ) {
const req = await fetch(
ppcp_add_payment_method.ajax
.subscription_change_payment_method.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax
.subscription_change_payment_method
.nonce,
subscription_id: subscriptionId,
payment_method: getCurrentPaymentMethod(),
wc_payment_token_id: result.data,
} ),
}
);
const res = await req.json();
if ( res.success === true ) {
window.location.href = `${ ppcp_add_payment_method.view_subscriptions_page }/${ subscriptionId }`;
return;
}
}
return;
}
window.location.href =
ppcp_add_payment_method.payment_methods_page;
return;
}
this.errorHandler.message( ppcp_add_payment_method.error_message );
},
onError: ( error ) => {
console.error( error );
errorHandler.message( ppcp_add_payment_method.error_message );
},
};
}
export function addPaymentMethodConfiguration( ppcp_add_payment_method ) {
return {
createVaultSetupToken: async () => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_setup_token.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax.create_setup_token
.nonce,
payment_method: getCurrentPaymentMethod(),
} ),
}
);
const result = await response.json();
if ( result.data.id ) {
return result.data.id;
}
console.error( result );
},
onApprove: async ( { vaultSetupToken } ) => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_payment_token_for_guest
.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax
.create_payment_token_for_guest.nonce,
vault_setup_token: vaultSetupToken,
} ),
}
);
const result = await response.json();
if ( result.success === true ) {
document.querySelector( '#place_order' ).click();
return;
}
console.error( result );
},
onError: ( error ) => {
console.error( error );
},
};
}

View file

@ -4,303 +4,108 @@ import {
PaymentMethods,
} from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
import { loadScript } from '@paypal/paypal-js';
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
import { buttonConfiguration, cardFieldsConfiguration } from './Configuration';
import { renderFields } from '../../../ppcp-card-fields/resources/js/Render';
import {
setVisible,
setVisibleByClass,
} from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
import { cardFieldStyles } from '../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper';
const errorHandler = new ErrorHandler(
ppcp_add_payment_method.labels.error.generic,
document.querySelector( '.woocommerce-notices-wrapper' )
);
const init = () => {
setVisibleByClass(
ORDER_BUTTON_SELECTOR,
getCurrentPaymentMethod() !== PaymentMethods.PAYPAL,
'ppcp-hidden'
);
setVisible(
`#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`,
getCurrentPaymentMethod() === PaymentMethods.PAYPAL
);
};
document.addEventListener( 'DOMContentLoaded', () => {
jQuery( document.body ).on(
'click init_add_payment_method',
'.payment_methods input.input-radio',
function () {
init();
}
);
if ( ppcp_add_payment_method.is_subscription_change_payment_page ) {
const saveToAccount = document.querySelector(
'#wc-ppcp-credit-card-gateway-new-payment-method'
);
if ( saveToAccount ) {
saveToAccount.checked = true;
saveToAccount.disabled = true;
}
}
setTimeout( () => {
loadScript( {
clientId: ppcp_add_payment_method.client_id,
merchantId: ppcp_add_payment_method.merchant_id,
dataUserIdToken: ppcp_add_payment_method.id_token,
components: 'buttons,card-fields',
} ).then( ( paypal ) => {
errorHandler.clear();
const paypalButtonContainer = document.querySelector(
`#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`
);
if ( paypalButtonContainer ) {
paypal
.Buttons( {
createVaultSetupToken: async () => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_setup_token
.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax
.create_setup_token.nonce,
} ),
}
);
const result = await response.json();
if ( result.data.id ) {
return result.data.id;
}
errorHandler.message(
ppcp_add_payment_method.error_message
);
},
onApprove: async ( { vaultSetupToken } ) => {
const response = await fetch(
ppcp_add_payment_method.ajax
.create_payment_token.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax
.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
} ),
}
);
const result = await response.json();
if ( result.success === true ) {
window.location.href =
ppcp_add_payment_method.payment_methods_page;
return;
}
errorHandler.message(
ppcp_add_payment_method.error_message
);
},
onError: ( error ) => {
console.error( error );
errorHandler.message(
ppcp_add_payment_method.error_message
);
},
} )
.render(
`#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`
);
( function ( { ppcp_add_payment_method, jQuery } ) {
document.addEventListener( 'DOMContentLoaded', () => {
jQuery( document.body ).on(
'click init_add_payment_method',
'.payment_methods input.input-radio',
function () {
setVisibleByClass(
ORDER_BUTTON_SELECTOR,
getCurrentPaymentMethod() !== PaymentMethods.PAYPAL,
'ppcp-hidden'
);
setVisible(
`#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`,
getCurrentPaymentMethod() === PaymentMethods.PAYPAL
);
}
);
const cardField = paypal.CardFields( {
createVaultSetupToken: async () => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_setup_token
.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax
.create_setup_token.nonce,
payment_method: PaymentMethods.CARDS,
verification_method:
ppcp_add_payment_method.verification_method,
} ),
}
);
// TODO move to wc subscriptions module
if ( ppcp_add_payment_method.is_subscription_change_payment_page ) {
const saveToAccount = document.querySelector(
'#wc-ppcp-credit-card-gateway-new-payment-method'
);
if ( saveToAccount ) {
saveToAccount.checked = true;
saveToAccount.disabled = true;
}
}
const result = await response.json();
if ( result.data.id ) {
return result.data.id;
}
setTimeout( () => {
loadScript( {
clientId: ppcp_add_payment_method.client_id,
merchantId: ppcp_add_payment_method.merchant_id,
dataUserIdToken: ppcp_add_payment_method.id_token,
components: 'buttons,card-fields',
} ).then( ( paypal ) => {
const errorHandler = new ErrorHandler(
ppcp_add_payment_method.labels.error.generic,
document.querySelector( '.woocommerce-notices-wrapper' )
);
errorHandler.clear();
errorHandler.message(
ppcp_add_payment_method.error_message
);
},
onApprove: async ( { vaultSetupToken } ) => {
const response = await fetch(
ppcp_add_payment_method.ajax.create_payment_token
.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax
.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
payment_method: PaymentMethods.CARDS,
} ),
}
);
const paypalButtonContainer = document.querySelector(
`#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`
);
const result = await response.json();
if ( result.success === true ) {
if ( paypalButtonContainer ) {
paypal
.Buttons(
buttonConfiguration(
ppcp_add_payment_method,
errorHandler
)
)
.render(
`#ppc-button-${ PaymentMethods.PAYPAL }-save-payment-method`
);
}
const cardFields = paypal.CardFields(
cardFieldsConfiguration(
ppcp_add_payment_method,
errorHandler
)
);
if ( cardFields.isEligible() ) {
renderFields( cardFields );
}
document
.querySelector( '#place_order' )
?.addEventListener( 'click', ( event ) => {
const cardPaymentToken = document.querySelector(
'input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked'
)?.value;
if (
ppcp_add_payment_method.is_subscription_change_payment_page
getCurrentPaymentMethod() !==
'ppcp-credit-card-gateway' ||
( cardPaymentToken && cardPaymentToken !== 'new' )
) {
const subscriptionId =
ppcp_add_payment_method.subscription_id_to_change_payment;
if ( subscriptionId && result.data ) {
const req = await fetch(
ppcp_add_payment_method.ajax
.subscription_change_payment_method
.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: ppcp_add_payment_method.ajax
.subscription_change_payment_method
.nonce,
subscription_id: subscriptionId,
payment_method:
getCurrentPaymentMethod(),
wc_payment_token_id: result.data,
} ),
}
);
const res = await req.json();
if ( res.success === true ) {
window.location.href = `${ ppcp_add_payment_method.view_subscriptions_page }/${ subscriptionId }`;
return;
}
}
return;
}
window.location.href =
ppcp_add_payment_method.payment_methods_page;
return;
}
event.preventDefault();
errorHandler.message(
ppcp_add_payment_method.error_message
);
},
onError: ( error ) => {
console.error( error );
errorHandler.message(
ppcp_add_payment_method.error_message
);
},
} );
if ( cardField.isEligible() ) {
const nameField = document.getElementById(
'ppcp-credit-card-gateway-card-name'
);
if ( nameField ) {
const styles = cardFieldStyles( nameField );
cardField
.NameField( { style: { input: styles } } )
.render( nameField.parentNode );
nameField.hidden = true;
}
const numberField = document.getElementById(
'ppcp-credit-card-gateway-card-number'
);
if ( numberField ) {
const styles = cardFieldStyles( numberField );
cardField
.NumberField( { style: { input: styles } } )
.render( numberField.parentNode );
numberField.hidden = true;
}
const expiryField = document.getElementById(
'ppcp-credit-card-gateway-card-expiry'
);
if ( expiryField ) {
const styles = cardFieldStyles( expiryField );
cardField
.ExpiryField( { style: { input: styles } } )
.render( expiryField.parentNode );
expiryField.hidden = true;
}
const cvvField = document.getElementById(
'ppcp-credit-card-gateway-card-cvc'
);
if ( cvvField ) {
const styles = cardFieldStyles( cvvField );
cardField
.CVVField( { style: { input: styles } } )
.render( cvvField.parentNode );
cvvField.hidden = true;
}
}
document
.querySelector( '#place_order' )
?.addEventListener( 'click', ( event ) => {
const cardPaymentToken = document.querySelector(
'input[name="wc-ppcp-credit-card-gateway-payment-token"]:checked'
)?.value;
if (
getCurrentPaymentMethod() !==
'ppcp-credit-card-gateway' ||
( cardPaymentToken && cardPaymentToken !== 'new' )
) {
return;
}
event.preventDefault();
cardField.submit().catch( ( error ) => {
console.error( error );
cardFields.submit().catch( ( error ) => {
console.error( error );
} );
} );
} );
} );
}, 1000 );
} );
}, 1000 );
} );
} )( {
ppcp_add_payment_method: window.ppcp_add_payment_method,
jQuery: window.jQuery,
} );

View file

@ -115,6 +115,11 @@ class CreatePaymentToken implements EndpointInterface {
if ( isset( $result->payment_source->card ) ) {
$wc_token_id = $this->wc_payment_tokens->create_payment_token_card( $current_user_id, $result );
$is_free_trial_cart = $data['is_free_trial_cart'] ?? '';
if ( $is_free_trial_cart === '1' ) {
WC()->session->set( 'ppcp_card_payment_token_for_free_trial', $wc_token_id );
}
}
}

View file

@ -0,0 +1,58 @@
/**
* Helper component to log debug details to the browser console.
*
* A utility class that is used by payment buttons on the front-end, like the GooglePayButton.
*/
export default class ConsoleLogger {
/**
* The prefix to display before every log output.
*
* @type {string}
*/
#prefix = '';
/**
* Whether logging is enabled, disabled by default.
*
* @type {boolean}
*/
#enabled = false;
constructor( ...prefixes ) {
if ( prefixes.length ) {
this.#prefix = `[${ prefixes.join( ' | ' ) }]`;
}
}
/**
* Enable or disable logging. Only impacts `log()` output.
*
* @param {boolean} state True to enable log output.
*/
set enabled( state ) {
this.#enabled = state;
}
/**
* Output log-level details to the browser console, if logging is enabled.
*
* @param {...any} args - All provided values are output to the browser console.
*/
log( ...args ) {
if ( this.#enabled ) {
// eslint-disable-next-line
console.log( this.#prefix, ...args );
}
}
/**
* Generate an error message in the browser's console.
*
* Error messages are always output, even when logging is disabled.
*
* @param {...any} args - All provided values are output to the browser console.
*/
error( ...args ) {
console.error( this.#prefix, ...args );
}
}

View file

@ -1,9 +1,11 @@
/* global jQuery */
/**
* Returns a Map with all input fields that are relevant to render the preview of the
* given payment button.
*
* @param {string} apmName - Value of the custom attribute `data-ppcp-apm-name`.
* @return {Map<string, {val:Function, el:HTMLInputElement}>}
* @return {Map<string, {val:Function, el:HTMLInputElement}>} List of input elements found on the current admin page.
*/
export function getButtonFormFields( apmName ) {
const inputFields = document.querySelectorAll(
@ -28,9 +30,9 @@ export function getButtonFormFields( apmName ) {
/**
* Returns a function that triggers an update of the specified preview button, when invoked.
*
* @param {string} apmName
* @return {((object) => void)}
* @return {((object) => void)} Trigger-function; updates preview buttons when invoked.
*/
export function buttonRefreshTriggerFactory( apmName ) {
const eventName = `ppcp_paypal_render_preview_${ apmName }`;
@ -44,7 +46,7 @@ export function buttonRefreshTriggerFactory( apmName ) {
* Returns a function that gets the current form values of the specified preview button.
*
* @param {string} apmName
* @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}}
* @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}} Getter-function; returns preview config details when invoked.
*/
export function buttonSettingsGetterFactory( apmName ) {
const fields = getButtonFormFields( apmName );

View file

@ -71,6 +71,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
return array(
'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway {
@ -198,6 +199,7 @@ return array(
Settings::PAY_LATER_TAB_ID,
AxoGateway::ID,
GooglePayGateway::ID,
ApplePayGateway::ID,
),
true
);
@ -220,6 +222,7 @@ return array(
Settings::PAY_LATER_TAB_ID,
Settings::CONNECTION_TAB_ID,
GooglePayGateway::ID,
ApplePayGateway::ID,
),
true
);
@ -1411,10 +1414,10 @@ return array(
return $label;
},
'wcgateway.enable-dcc-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/entry/product/ppcp';
return 'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp';
},
'wcgateway.enable-dcc-url-live' => static function ( ContainerInterface $container ): string {
return 'https://www.paypal.com/bizsignup/entry/product/ppcp';
return 'https://www.paypal.com/bizsignup/entry?product=ppcp';
},
'wcgateway.enable-pui-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/entry?country.x=DE&product=payment_methods&capabilities=PAY_UPON_INVOICE';

View file

@ -426,12 +426,42 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
WC()->session->set( 'ppcp_card_payment_token_for_free_trial', null );
return $this->handle_payment_failure(
null,
new GatewayGenericException( new Exception( 'WC order was not found.' ) )
);
}
$guest_card_payment_for_free_trial = WC()->session->get( 'ppcp_guest_payment_for_free_trial' ) ?? null;
WC()->session->get( 'ppcp_guest_payment_for_free_trial', null );
if ( $guest_card_payment_for_free_trial ) {
$customer_id = $guest_card_payment_for_free_trial->customer->id ?? '';
if ( $customer_id ) {
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
}
if ( isset( $guest_card_payment_for_free_trial->payment_source->card ) ) {
$this->wc_payment_tokens->create_payment_token_card( $wc_order->get_customer_id(), $guest_card_payment_for_free_trial );
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
$card_payment_token_for_free_trial = WC()->session->get( 'ppcp_card_payment_token_for_free_trial' ) ?? null;
WC()->session->set( 'ppcp_card_payment_token_for_free_trial', null );
if ( $card_payment_token_for_free_trial ) {
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() );
foreach ( $tokens as $token ) {
if ( $token->get_id() === (int) $card_payment_token_for_free_trial ) {
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$card_payment_token_id = wc_clean( wp_unslash( $_POST['wc-ppcp-credit-card-gateway-payment-token'] ?? '' ) );

View file

@ -118,18 +118,15 @@ class Settings implements ContainerInterface {
* Stores the settings to the database.
*/
public function persist() {
return update_option( self::KEY, $this->settings );
}
/**
* Loads the settings.
*
* @return bool
*/
private function load(): bool {
if ( $this->settings ) {
return false;
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;