Merge branch 'trunk'

# Conflicts:
#	modules/ppcp-admin-notices/src/AdminNotices.php
#	modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php
#	modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php
#	modules/ppcp-wc-gateway/src/WCGatewayModule.php
This commit is contained in:
Philipp Stracker 2024-08-30 15:16:20 +02:00
commit 7509f914ab
No known key found for this signature in database
67 changed files with 3811 additions and 126 deletions

View file

@ -1 +0,0 @@
-s:*

View file

@ -1,5 +1,25 @@
*** Changelog ***
= 2.9.0 - xxxx-xx-xx =
* Fix - Fatal error in Block Editor when using WooCommerce blocks #2534
* Fix - Can't pay from block pages when the shipping callback is enabled and no shipping methods defined #2429
* Fix - Various Google Pay button fixes #2496
* Fix - Buying a free trial subscription with ACDC results in a $1 charge in the API call #2465
* Fix - Problem with Google Pay and Apple Pay button placement on Pay for Order page #2542
* Fix - When there isn't any shipping option for the address the order is still created from classic cart #2437
* Fix - Patch the order with no shipping methods, instead of throwing an error #2435
* Enhancement - Separate Apple Pay button for Classic Checkout #2457
* Enhancement - Remove AMEX support for ACDC when store location is set to China #2526
* Enhancement - Inform users of Pay Later messaging configuration when Pay Later wasn't recently enabled #2529
* Enhancement - Update ACDC signup URLs #2475
* Enhancement - Implement country based APMs via Orders API #2511
* Enhancement - Update PaymentsStatusHandlingTrait.php (author @callmeahmedr) #2523
* Enhancement - Disable PayPal Shipping callback by default #2527
* Enhancement - Change Apple Pay and Google Pay default button labels to plain #2476
* Enhancement - Add Package Tracking compatibility with DHL Shipping plugin #2463
* Enhancement - Add support for WC Bookings when skipping checkout confirmation #2452
* Enhancement - Remove currencies from country-currency matrix in card fields module #2441
= 2.8.3 - 2024-08-12 =
* Fix - Google Pay: Prevent field validation from being triggered on checkout page load #2474
* Fix - Do not add tax info into order meta during order creation #2471

View file

@ -31,6 +31,7 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-uninstall/module.php" )(),
( require "$modules_dir/ppcp-blocks/module.php" )(),
( require "$modules_dir/ppcp-paypal-subscriptions/module.php" )(),
( require "$modules_dir/ppcp-local-alternative-payment-methods/module.php" )(),
);
// phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
@ -88,12 +89,5 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-axo/module.php" )();
}
if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.local_apms_enabled',
getenv( 'PCP_LOCAL_APMS_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-local-alternative-payment-methods/module.php" )();
}
return $modules;
};

View file

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

View file

@ -0,0 +1,31 @@
{
"name": "ppcp-admin-notices",
"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,10 @@
.notice.is-dismissible {
.spinner.doing-ajax {
position: absolute;
z-index: 1;
right: 0;
top: 0;
margin: 9px;
pointer-events: none;
}
}

View file

@ -0,0 +1,137 @@
export default class DismissibleMessage {
#notice = null;
#muteConfig = {};
#closeButton = null;
#msgId = '';
constructor( noticeElement, muteConfig ) {
this.#notice = noticeElement;
this.#muteConfig = muteConfig;
this.#msgId = this.#notice.dataset.ppcpMsgId;
// Quick sanitation.
if ( ! this.#muteConfig?.endpoint || ! this.#muteConfig?.nonce ) {
console.error( 'Ajax config (Mute):', this.#muteConfig );
throw new Error(
'Invalid ajax configuration for DismissibleMessage. Nonce/Endpoint missing'
);
}
if ( ! this.#msgId ) {
console.error( 'Notice Element:', this.#notice );
throw new Error(
'Invalid notice element passed to DismissibleMessage. No MsgId defined'
);
}
this.onDismissClickProxy = this.onDismissClickProxy.bind( this );
this.enableCloseButtons = this.enableCloseButtons.bind( this );
this.disableCloseButtons = this.disableCloseButtons.bind( this );
this.dismiss = this.dismiss.bind( this );
this.addEventListeners();
}
get id() {
return this.#msgId;
}
get closeButton() {
if ( ! this.#closeButton ) {
this.#closeButton = this.#notice.querySelector(
'button.notice-dismiss'
);
}
return this.#closeButton;
}
addEventListeners() {
this.#notice.addEventListener(
'click',
this.onDismissClickProxy,
true
);
}
removeEventListeners() {
this.#notice.removeEventListener(
'click',
this.onDismissClickProxy,
true
);
}
onDismissClickProxy( event ) {
if ( ! event.target?.matches( 'button.notice-dismiss' ) ) {
return;
}
this.disableCloseButtons();
this.muteMessage();
event.preventDefault();
event.stopPropagation();
return false;
}
disableCloseButtons() {
this.closeButton.setAttribute( 'disabled', 'disabled' );
this.closeButton.style.pointerEvents = 'none';
this.closeButton.style.opacity = 0;
}
enableCloseButtons() {
this.closeButton.removeAttribute( 'disabled', 'disabled' );
this.closeButton.style.pointerEvents = '';
this.closeButton.style.opacity = '';
}
showSpinner() {
const spinner = document.createElement( 'span' );
spinner.classList.add( 'spinner', 'is-active', 'doing-ajax' );
this.#notice.appendChild( spinner );
}
/**
* Mute the message (on server side) and dismiss it (in browser).
*/
muteMessage() {
this.#ajaxMuteMessage().then( this.dismiss );
}
/**
* Start an ajax request that marks the message as "muted" on server side.
*
* @return {Promise<any>} Resolves after the ajax request is completed.
*/
#ajaxMuteMessage() {
this.showSpinner();
const ajaxData = {
id: this.id,
nonce: this.#muteConfig.nonce,
};
return fetch( this.#muteConfig.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( ajaxData ),
} ).then( ( response ) => response.json() );
}
/**
* Proxy to the original dismiss logic provided by WP core JS.
*/
dismiss() {
this.removeEventListeners();
this.enableCloseButtons();
this.closeButton.dispatchEvent( new Event( 'click' ) );
}
}

View file

@ -0,0 +1,27 @@
import DismissibleMessage from './DismissibleMessage';
class AdminMessageHandler {
#config = {};
constructor( config ) {
this.#config = config;
this.setupDismissibleMessages();
}
/**
* Finds all mutable admin messages in the DOM and initializes them.
*/
setupDismissibleMessages() {
const muteConfig = this.#config?.ajax?.mute_message;
const addDismissibleMessage = ( element ) => {
new DismissibleMessage( element, muteConfig );
};
document
.querySelectorAll( '.notice[data-ppcp-msg-id]' )
.forEach( addDismissibleMessage );
}
}
new AdminMessageHandler( window.wc_admin_notices );

View file

@ -14,15 +14,33 @@ use WooCommerce\PayPalCommerce\AdminNotices\Renderer\Renderer;
use WooCommerce\PayPalCommerce\AdminNotices\Renderer\RendererInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\RepositoryInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Endpoint\MuteMessageEndpoint;
return array(
'admin-notices.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
}
return plugins_url(
'/modules/ppcp-admin-notices/',
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'admin-notices.renderer' => static function ( ContainerInterface $container ): RendererInterface {
$repository = $container->get( 'admin-notices.repository' );
return new Renderer( $repository );
return new Renderer(
$container->get( 'admin-notices.repository' ),
$container->get( 'admin-notices.url' ),
$container->get( 'ppcp.asset-version' )
);
},
'admin-notices.repository' => static function ( ContainerInterface $container ): RepositoryInterface {
return new Repository();
},
'admin-notices.mute-message-endpoint' => static function ( ContainerInterface $container ): MuteMessageEndpoint {
return new MuteMessageEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'admin-notices.repository' )
);
},
);

View file

@ -9,13 +9,15 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
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\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Endpoint\MuteMessageEndpoint;
use WooCommerce\PayPalCommerce\AdminNotices\Renderer\RendererInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class AdminNotices
@ -41,10 +43,12 @@ class AdminNotices implements ServiceModule, ExtendingModule, ExecutableModule {
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
$renderer = $c->get( 'admin-notices.renderer' );
assert( $renderer instanceof RendererInterface );
add_action(
'admin_notices',
function() use ( $c ) {
$renderer = $c->get( 'admin-notices.renderer' );
function() use ( $renderer ) {
$renderer->render();
}
);
@ -77,6 +81,35 @@ class AdminNotices implements ServiceModule, ExtendingModule, ExecutableModule {
}
);
/**
* Since admin notices are rendered after the initial `admin_enqueue_scripts`
* action fires, we use the `admin_footer` hook to enqueue the optional assets
* for admin-notices in the page footer.
*/
add_action(
'admin_footer',
static function () use ( $renderer ) {
$renderer->enqueue_admin();
}
);
add_action(
'wp_ajax_' . MuteMessageEndpoint::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'admin-notices.mute-message-endpoint' );
assert( $endpoint instanceof MuteMessageEndpoint );
$endpoint->handle_request();
}
);
add_action(
'woocommerce_paypal_payments_uninstall',
static function () {
PersistentMessage::clear_all();
}
);
return true;
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* Permanently mutes an admin notification for the current user.
*
* @package WooCommerce\PayPalCommerce\AdminNotices\Endpoint
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Endpoint;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class MuteMessageEndpoint
*/
class MuteMessageEndpoint {
const ENDPOINT = 'ppc-mute-message';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* Message repository to retrieve the message object to mute.
*
* @var Repository
*/
private $message_repository;
/**
* UpdateShippingEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param Repository $message_repository Message repository, to access messages.
*/
public function __construct(
RequestData $request_data,
Repository $message_repository
) {
$this->request_data = $request_data;
$this->message_repository = $message_repository;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce() : string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return void
*/
public function handle_request() : void {
try {
$data = $this->request_data->read_request( $this->nonce() );
} catch ( RuntimeException $ex ) {
wp_send_json_error();
}
$id = $data['id'] ?? '';
if ( ! $id || ! is_string( $id ) ) {
wp_send_json_error();
}
/**
* Create a dummy message with the provided ID and mark it as muted.
*
* This helps to keep code cleaner and make the mute-endpoint more reliable,
* as other modules do not need to register the PersistentMessage on every
* ajax request.
*/
$message = new PersistentMessage( $id, '', '', '' );
$message->mute();
wp_send_json_success();
}
}

View file

@ -5,7 +5,7 @@
* @package WooCommerce\PayPalCommerce\AdminNotices\Entity
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Entity;
@ -15,7 +15,7 @@ namespace WooCommerce\PayPalCommerce\AdminNotices\Entity;
class Message {
/**
* The messagte text.
* The message text.
*
* @var string
*/
@ -29,11 +29,11 @@ class Message {
private $type;
/**
* Whether the message is dismissable.
* Whether the message is dismissible.
*
* @var bool
*/
private $dismissable;
private $dismissible;
/**
* The wrapper selector that will contain the notice.
@ -47,13 +47,13 @@ class Message {
*
* @param string $message The message text.
* @param string $type The message type.
* @param bool $dismissable Whether the message is dismissable.
* @param bool $dismissible Whether the message is dismissible.
* @param string $wrapper The wrapper selector that will contain the notice.
*/
public function __construct( string $message, string $type, bool $dismissable = true, string $wrapper = '' ) {
public function __construct( string $message, string $type, bool $dismissible = true, string $wrapper = '' ) {
$this->type = $type;
$this->message = $message;
$this->dismissable = $dismissable;
$this->dismissible = $dismissible;
$this->wrapper = $wrapper;
}
@ -62,7 +62,7 @@ class Message {
*
* @return string
*/
public function message(): string {
public function message() : string {
return $this->message;
}
@ -71,17 +71,17 @@ class Message {
*
* @return string
*/
public function type(): string {
public function type() : string {
return $this->type;
}
/**
* Returns whether the message is dismissable.
* Returns whether the message is dismissible.
*
* @return bool
*/
public function is_dismissable(): bool {
return $this->dismissable;
public function is_dismissible() : bool {
return $this->dismissible;
}
/**
@ -89,21 +89,37 @@ class Message {
*
* @return string
*/
public function wrapper(): string {
public function wrapper() : string {
return $this->wrapper;
}
/**
* Returns the object as array.
* Returns the object as array, for serialization.
*
* @return array
*/
public function to_array(): array {
public function to_array() : array {
return array(
'type' => $this->type,
'message' => $this->message,
'dismissable' => $this->dismissable,
'dismissible' => $this->dismissible,
'wrapper' => $this->wrapper,
);
}
/**
* Converts a plain array to a full Message instance, during deserialization.
*
* @param array $data Data generated by `Message::to_array()`.
*
* @return Message
*/
public static function from_array( array $data ) : Message {
return new Message(
(string) ( $data['message'] ?? '' ),
(string) ( $data['type'] ?? '' ),
(bool) ( $data['dismissible'] ?? true ),
(string) ( $data['wrapper'] ?? '' )
);
}
}

View file

@ -0,0 +1,125 @@
<?php
/**
* Extends the Message class to permanently dismiss notices for single users.
*
* @package WooCommerce\PayPalCommerce\AdminNotices\Entity
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Entity;
/**
* Class PersistentMessage
*/
class PersistentMessage extends Message {
/**
* Prefix for DB keys to store IDs of permanently muted notices.
*/
public const USER_META_PREFIX = '_ppcp_notice_';
/**
* An internal ID to permanently dismiss the persistent message.
*
* @var string
*/
private $message_id;
/**
* Message constructor.
*
* @param string $id ID of this message, to allow permanent dismissal.
* @param string $message The message text.
* @param string $type The message type.
* @param string $wrapper The wrapper selector that will contain the notice.
*/
public function __construct( string $id, string $message, string $type, string $wrapper = '' ) {
parent::__construct( $message, $type, true, $wrapper );
$this->message_id = sanitize_key( $id );
}
/**
* Returns the sanitized ID that identifies a permanently dismissible message.
*
* @param bool $with_db_prefix Whether to add the user-meta prefix.
*
* @return string
*/
public function id( bool $with_db_prefix = false ) : string {
if ( ! $this->message_id ) {
return '';
}
return $with_db_prefix ? self::USER_META_PREFIX . $this->message_id : $this->message_id;
}
/**
* {@inheritDoc}
*/
public function to_array() : array {
$data = parent::to_array();
$data['id'] = $this->message_id;
return $data;
}
/**
* {@inheritDoc}
*
* @return PersistentMessage
*/
public static function from_array( array $data ) : Message {
return new PersistentMessage(
(string) ( $data['id'] ?? '' ),
(string) ( $data['message'] ?? '' ),
(string) ( $data['type'] ?? '' ),
(string) ( $data['wrapper'] ?? '' )
);
}
/**
* Whether the message was permanently muted by the current user.
*
* @return bool
*/
public function is_muted() : bool {
$user_id = get_current_user_id();
if ( ! $this->message_id || ! $user_id ) {
return false;
}
return 0 < (int) get_user_meta( $user_id, $this->id( true ), true );
}
/**
* Mark the message as permanently muted by the current user.
*
* @return void
*/
public function mute() : void {
$user_id = get_current_user_id();
if ( $this->message_id && $user_id && ! $this->is_muted() ) {
update_user_meta( $user_id, $this->id( true ), time() );
}
}
/**
* Removes all user-meta flags for muted messages.
*
* @return void
*/
public static function clear_all() : void {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $wpdb->usermeta WHERE meta_key LIKE %s",
$wpdb->esc_like( self::USER_META_PREFIX ) . '%'
)
);
}
}

View file

@ -10,6 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices\Renderer;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\RepositoryInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Endpoint\MuteMessageEndpoint;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class Renderer
@ -23,32 +25,123 @@ class Renderer implements RendererInterface {
*/
private $repository;
/**
* Used to enqueue assets.
*
* @var string
*/
private $module_url;
/**
* Used to enqueue assets.
*
* @var string
*/
private $version;
/**
* Whether the current page contains at least one message that can be muted.
*
* @var bool
*/
private $can_mute_message = false;
/**
* Renderer constructor.
*
* @param RepositoryInterface $repository The message repository.
* @param string $module_url The module URL.
* @param string $version The module version.
*/
public function __construct( RepositoryInterface $repository ) {
public function __construct(
RepositoryInterface $repository,
string $module_url,
string $version
) {
$this->repository = $repository;
$this->module_url = untrailingslashit( $module_url );
$this->version = $version;
}
/**
* Renders the current messages.
*
* @return bool
* {@inheritDoc}
*/
public function render(): bool {
$messages = $this->repository->current_message();
foreach ( $messages as $message ) {
$mute_message_id = '';
if ( $message instanceof PersistentMessage ) {
$this->can_mute_message = true;
$mute_message_id = $message->id();
}
printf(
'<div class="notice notice-%s %s" %s><p>%s</p></div>',
'<div class="notice notice-%s %s" %s%s><p>%s</p></div>',
$message->type(),
( $message->is_dismissable() ) ? 'is-dismissible' : '',
( $message->is_dismissible() ) ? 'is-dismissible' : '',
( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ),
// Use `empty()` in condition, to avoid false phpcs warning.
( empty( $mute_message_id ) ? '' : sprintf( 'data-ppcp-msg-id="%s"', esc_attr( $mute_message_id ) ) ),
wp_kses_post( $message->message() )
);
}
return (bool) count( $messages );
}
/**
* {@inheritDoc}
*/
public function enqueue_admin() : void {
if ( ! $this->can_mute_message ) {
return;
}
wp_register_style(
'wc-ppcp-admin-notice',
$this->module_url . '/assets/css/styles.css',
array(),
$this->version
);
wp_register_script(
'wc-ppcp-admin-notice',
$this->module_url . '/assets/js/boot-admin.js',
array(),
$this->version,
true
);
wp_localize_script(
'wc-ppcp-admin-notice',
'wc_admin_notices',
$this->script_data_for_admin()
);
wp_enqueue_style( 'wc-ppcp-admin-notice' );
wp_enqueue_script( 'wc-ppcp-admin-notice' );
}
/**
* Data to inject into the current admin page, which is required by JS assets.
*
* @return array
*/
protected function script_data_for_admin() : array {
$ajax_url = admin_url( 'admin-ajax.php' );
return array(
'ajax' => array(
'mute_message' => array(
'endpoint' => add_query_arg(
array( 'action' => MuteMessageEndpoint::ENDPOINT ),
$ajax_url
),
'nonce' => wp_create_nonce( MuteMessageEndpoint::nonce() ),
),
),
);
}
}

View file

@ -20,4 +20,11 @@ interface RendererInterface {
* @return bool
*/
public function render(): bool;
/**
* Enqueues common assets required for the admin notice behavior.
*
* @return void
*/
public function enqueue_admin() : void;
}

View file

@ -5,11 +5,12 @@
* @package WooCommerce\PayPalCommerce\AdminNotices\Repository
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Repository;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class Repository
@ -20,11 +21,11 @@ class Repository implements RepositoryInterface {
const PERSISTED_NOTICES_OPTION = 'woocommerce_ppcp-admin-notices';
/**
* Returns the current messages.
* Returns current messages to display, which excludes muted messages.
*
* @return Message[]
*/
public function current_message(): array {
public function current_message() : array {
return array_filter(
/**
* Returns the list of admin messages.
@ -33,7 +34,11 @@ class Repository implements RepositoryInterface {
self::NOTICES_FILTER,
array()
),
function( $element ) : bool {
function ( $element ) : bool {
if ( $element instanceof PersistentMessage ) {
return ! $element->is_muted();
}
return is_a( $element, Message::class );
}
);
@ -43,9 +48,10 @@ class Repository implements RepositoryInterface {
* Adds a message to persist between page reloads.
*
* @param Message $message The message.
*
* @return void
*/
public function persist( Message $message ): void {
public function persist( Message $message ) : void {
$persisted_notices = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
$persisted_notices[] = $message->to_array();
@ -58,20 +64,18 @@ class Repository implements RepositoryInterface {
*
* @return array|Message[]
*/
public function get_persisted_and_clear(): array {
public function get_persisted_and_clear() : array {
$notices = array();
$persisted_data = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
foreach ( $persisted_data as $notice_data ) {
$notices[] = new Message(
(string) ( $notice_data['message'] ?? '' ),
(string) ( $notice_data['type'] ?? '' ),
(bool) ( $notice_data['dismissable'] ?? true ),
(string) ( $notice_data['wrapper'] ?? '' )
);
if ( is_array( $notice_data ) ) {
$notices[] = Message::from_array( $notice_data );
}
}
update_option( self::PERSISTED_NOTICES_OPTION, array(), true );
return $notices;
}
}

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: {
'boot-admin': path.resolve( './resources/js/boot-admin.js' ),
"styles": path.resolve('./resources/css/styles.scss')
},
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

@ -96,9 +96,107 @@ class Orders {
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
$body = json_decode( $response['body'] );
$message = $body->details[0]->description ?? '';
if ( $message ) {
throw new RuntimeException( $message );
}
throw new PayPalApiException(
json_decode( $response['body'] ),
$body,
$status_code
);
}
return $response;
}
/**
* Confirms the given order.
*
* @link https://developer.paypal.com/docs/api/orders/v2/#orders_confirm
*
* @param array $request_body The request body.
* @param string $id PayPal order ID.
* @return array
* @throws RuntimeException If something went wrong with the request.
* @throws PayPalApiException If something went wrong with the PayPal API request.
*/
public function confirm_payment_source( array $request_body, string $id ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id . '/confirm-payment-source';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'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 ) {
$body = json_decode( $response['body'] );
$message = $body->details[0]->description ?? '';
if ( $message ) {
throw new RuntimeException( $message );
}
throw new PayPalApiException(
$body,
$status_code
);
}
return $response;
}
/**
* Get PayPal order by id.
*
* @param string $id PayPal order ID.
* @return array
* @throws RuntimeException If something went wrong with the request.
* @throws PayPalApiException If something went wrong with the PayPal API request.
*/
public function order( string $id ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
);
$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 ) {
$body = json_decode( $response['body'] );
$message = $body->details[0]->description ?? '';
if ( $message ) {
throw new RuntimeException( $message );
}
throw new PayPalApiException(
$body,
$status_code
);
}

View file

@ -339,6 +339,12 @@ class ApplePayButton {
this.#isInitialized = true;
this.applePayConfig = config;
if ( this.isSeparateGateway ) {
document
.querySelectorAll( '#ppc-button-applepay-container' )
.forEach( ( el ) => el.remove() );
}
if ( ! this.isEligible ) {
this.hide();
} else {

View file

@ -306,7 +306,8 @@ return array(
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'session.handler' ),
$container->get( 'applepay.url' )
$container->get( 'applepay.url' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},

View file

@ -10,6 +10,7 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Applepay;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
@ -72,6 +73,13 @@ class ApplePayGateway extends WC_Payment_Gateway {
*/
private $module_url;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* ApplePayGateway constructor.
*
@ -84,6 +92,7 @@ class ApplePayGateway extends WC_Payment_Gateway {
* view URL based on order.
* @param SessionHandler $session_handler The Session Handler.
* @param string $module_url The URL to the module.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
OrderProcessor $order_processor,
@ -91,7 +100,8 @@ class ApplePayGateway extends WC_Payment_Gateway {
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider,
SessionHandler $session_handler,
string $module_url
string $module_url,
LoggerInterface $logger
) {
$this->id = self::ID;
@ -111,6 +121,7 @@ class ApplePayGateway extends WC_Payment_Gateway {
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
$this->session_handler = $session_handler;
$this->logger = $logger;
add_action(
'woocommerce_update_options_payment_gateways_' . $this->id,

View file

@ -113,6 +113,13 @@ export default class PaymentButton {
*/
#isInitialized = false;
/**
* Whether the one-time initialization of the payment gateway is complete.
*
* @type {boolean}
*/
#gatewayInitialized = false;
/**
* The button's context.
*
@ -437,6 +444,17 @@ export default class PaymentButton {
return this.wrappers.Default;
}
/**
* Whether the button is placed inside a classic gateway context.
*
* Classic gateway contexts are: Classic checkout, Pay for Order page.
*
* @return {boolean} True indicates, the button is located inside a classic gateway.
*/
get isInsideClassicGateway() {
return PaymentContext.Gateways.includes( this.context );
}
/**
* 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
@ -449,20 +467,21 @@ export default class PaymentButton {
get isSeparateGateway() {
return (
this.#buttonConfig.is_wc_gateway_enabled &&
PaymentContext.Gateways.includes( this.context )
this.isInsideClassicGateway
);
}
/**
* Whether the currently selected payment gateway is set to the payment method.
*
* Only relevant on checkout pages, when `this.isSeparateGateway` is true.
* Only relevant on checkout pages where "classic" payment gateways are rendered.
*
* @return {boolean} True means that this payment method is selected as current gateway.
*/
get isCurrentGateway() {
if ( ! this.isSeparateGateway ) {
return false;
if ( ! this.isInsideClassicGateway ) {
// This means, the button's visibility is managed by another script.
return true;
}
/*
@ -470,7 +489,14 @@ export default class PaymentButton {
* 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();
const currentMethod = getCurrentPaymentMethod();
if ( this.isSeparateGateway ) {
return this.methodId === currentMethod;
}
// Button is rendered inside the Smart Buttons block.
return PaymentMethods.PAYPAL === currentMethod;
}
/**
@ -673,7 +699,7 @@ export default class PaymentButton {
} );
// Events relevant for buttons inside a payment gateway.
if ( PaymentContext.Gateways.includes( this.context ) ) {
if ( this.isInsideClassicGateway ) {
const parentMethod = this.isSeparateGateway
? this.methodId
: PaymentMethods.PAYPAL;
@ -703,7 +729,7 @@ export default class PaymentButton {
this.applyWrapperStyles();
if ( this.isEligible && this.isPresent && this.isVisible ) {
if ( this.isEligible && this.isCurrentGateway && this.isVisible ) {
if ( ! this.isButtonAttached ) {
this.log( 'refresh.addButton' );
this.addButton();
@ -712,25 +738,33 @@ export default class PaymentButton {
}
/**
* Makes the custom payment gateway visible by removing initial inline styles from the DOM.
* Makes the payment gateway visible by removing initial inline styles from the DOM.
* Also, removes the button-placeholder container from the smart button block.
*
* Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true`
*/
showPaymentGateway() {
if ( ! this.isSeparateGateway || ! this.isEligible ) {
if (
this.#gatewayInitialized ||
! this.isSeparateGateway ||
! this.isEligible
) {
return;
}
const styleSelectors = `style[data-hide-gateway="${ this.methodId }"]`;
const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
const wrapperSelector = `#${ this.wrappers.Default }`;
const styles = document.querySelectorAll( styleSelectors );
document
.querySelectorAll( styleSelector )
.forEach( ( el ) => el.remove() );
document
.querySelectorAll( wrapperSelector )
.forEach( ( el ) => el.remove() );
if ( ! styles.length ) {
return;
}
this.log( 'Show gateway' );
styles.forEach( ( el ) => el.remove() );
this.#gatewayInitialized = true;
// This code runs only once, during button initialization, and fixes the initial visibility.
this.isVisible = this.isCurrentGateway;

View file

@ -267,7 +267,8 @@ return array(
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'session.handler' ),
$container->get( 'googlepay.url' )
$container->get( 'googlepay.url' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -72,6 +73,13 @@ class GooglePayGateway extends WC_Payment_Gateway {
*/
private $module_url;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* GooglePayGateway constructor.
*
@ -81,6 +89,7 @@ class GooglePayGateway extends WC_Payment_Gateway {
* @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.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
OrderProcessor $order_processor,
@ -88,7 +97,8 @@ class GooglePayGateway extends WC_Payment_Gateway {
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider,
SessionHandler $session_handler,
string $module_url
string $module_url,
LoggerInterface $logger
) {
$this->id = self::ID;
@ -113,6 +123,7 @@ class GooglePayGateway extends WC_Payment_Gateway {
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
$this->session_handler = $session_handler;
$this->logger = $logger;
add_action(
'woocommerce_update_options_payment_gateways_' . $this->id,

View file

@ -0,0 +1,18 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { APM } from './apm-block';
const config = wc.wcSettings.getSetting( 'ppcp-multibanco_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

@ -60,6 +60,11 @@ return array(
'countries' => array( 'AT', 'DE', 'DK', 'EE', 'ES', 'FI', 'GB', 'LT', 'LV', 'NL', 'NO', 'SE' ),
'currencies' => array( 'EUR', 'DKK', 'SEK', 'GBP', 'NOK' ),
),
'multibanco' => array(
'id' => MultibancoGateway::ID,
'countries' => array( 'PT' ),
'currencies' => array( 'EUR' ),
),
);
},
'ppcp-local-apms.bancontact.wc-gateway' => static function ( ContainerInterface $container ): BancontactGateway {
@ -118,6 +123,14 @@ return array(
$container->get( 'wcgateway.transaction-url-provider' )
);
},
'ppcp-local-apms.multibanco.wc-gateway' => static function ( ContainerInterface $container ): MultibancoGateway {
return new MultibancoGateway(
$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' ),
@ -167,4 +180,11 @@ return array(
$container->get( 'ppcp-local-apms.trustly.wc-gateway' )
);
},
'ppcp-local-apms.multibanco.payment-method' => static function( ContainerInterface $container ): MultibancoPaymentMethod {
return new MultibancoPaymentMethod(
$container->get( 'ppcp-local-apms.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'ppcp-local-apms.multibanco.wc-gateway' )
);
},
);

View file

@ -13,6 +13,7 @@ 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -72,7 +73,7 @@ class BancontactGateway extends WC_Payment_Gateway {
'products',
);
$this->method_title = __( 'Bancontact', 'woocommerce-paypal-payments' );
$this->method_title = __( 'Bancontact (via PayPal)', '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' ) );
@ -177,6 +178,9 @@ class BancontactGateway extends WC_Payment_Gateway {
$body = json_decode( $response['body'] );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {

View file

@ -91,7 +91,7 @@ class BancontactPaymentMethod extends AbstractPaymentMethodType {
'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' ),
'icon' => 'bancontact',
);
}
}

View file

@ -13,6 +13,7 @@ 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -72,7 +73,7 @@ class BlikGateway extends WC_Payment_Gateway {
'products',
);
$this->method_title = __( 'Blik', 'woocommerce-paypal-payments' );
$this->method_title = __( 'Blik (via PayPal)', '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' ) );
@ -178,6 +179,9 @@ class BlikGateway extends WC_Payment_Gateway {
$body = json_decode( $response['body'] );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {

View file

@ -91,7 +91,7 @@ class BlikPaymentMethod extends AbstractPaymentMethodType {
'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' ),
'icon' => 'blik',
);
}
}

View file

@ -13,6 +13,7 @@ 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -72,7 +73,7 @@ class EPSGateway extends WC_Payment_Gateway {
'products',
);
$this->method_title = __( 'EPS', 'woocommerce-paypal-payments' );
$this->method_title = __( 'EPS (via PayPal)', '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' ) );
@ -177,6 +178,9 @@ class EPSGateway extends WC_Payment_Gateway {
$body = json_decode( $response['body'] );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {

View file

@ -91,7 +91,7 @@ class EPSPaymentMethod extends AbstractPaymentMethodType {
'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' ),
'icon' => 'eps',
);
}
}

View file

@ -13,6 +13,7 @@ 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -72,7 +73,7 @@ class IDealGateway extends WC_Payment_Gateway {
'products',
);
$this->method_title = __( 'iDeal', 'woocommerce-paypal-payments' );
$this->method_title = __( 'iDeal (via PayPal)', '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' ) );
@ -134,16 +135,13 @@ class IDealGateway extends WC_Payment_Gateway {
$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,
'ideal' => 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(
@ -179,6 +177,9 @@ class IDealGateway extends WC_Payment_Gateway {
$body = json_decode( $response['body'] );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {

View file

@ -91,7 +91,7 @@ class IDealPaymentMethod extends AbstractPaymentMethodType {
'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' ),
'icon' => 'ideal',
);
}
}

View file

@ -16,6 +16,8 @@ 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\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\FeesUpdater;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class LocalAlternativePaymentMethodsModule
@ -37,11 +39,17 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ) : bool {
public function run( ContainerInterface $c ): bool {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( ! $settings->has( 'allow_local_apm_gateways' ) || $settings->get( 'allow_local_apm_gateways' ) !== true ) {
return true;
}
add_filter(
'woocommerce_payment_gateways',
/**
@ -158,6 +166,37 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
}
);
add_action(
'woocommerce_paypal_payments_payment_capture_completed_webhook_handler',
function( WC_Order $wc_order, string $order_id ) use ( $c ) {
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
if (
! $this->is_local_apm( $wc_order->get_payment_method(), $payment_methods )
) {
return;
}
$fees_updater = $c->get( 'wcgateway.helper.fees-updater' );
assert( $fees_updater instanceof FeesUpdater );
$fees_updater->update( $order_id, $wc_order );
},
10,
2
);
add_filter(
'woocommerce_paypal_payments_allowed_refund_payment_methods',
function( array $payment_methods ) use ( $c ): array {
$local_payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $local_payment_methods as $payment_method ) {
$payment_methods[] = $payment_method['id'];
}
return $payment_methods;
}
);
return true;
}

View file

@ -0,0 +1,236 @@
<?php
/**
* The Multibanco 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class MultibancoGateway
*/
class MultibancoGateway extends WC_Payment_Gateway {
const ID = 'ppcp-multibanco';
/**
* 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;
/**
* MultibancoGateway 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 = __( 'Multibanco (via PayPal)', 'woocommerce-paypal-payments' );
$this->method_description = __( 'An online payment method in Portugal, enabling Portuguese buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', __( 'Multibanco', 'woocommerce-paypal-payments' ) );
$this->description = $this->get_option( 'description', '' );
$this->icon = esc_url( 'https://www.paypalobjects.com/images/checkout/alternative_payments/paypal_multibanco_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' => __( 'Multibanco', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable Multibanco 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( 'pending', __( 'Awaiting for the buyer to complete 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',
'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(),
),
),
);
try {
$response = $this->orders_endpoint->create( $request_body );
$body = json_decode( $response['body'] );
$request_body = array(
'payment_source' => array(
'multibanco' => 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',
'application_context' => array(
'locale' => 'en-PT',
'return_url' => $this->get_return_url( $wc_order ),
'cancel_url' => add_query_arg( 'cancelled', 'true', $this->get_return_url( $wc_order ) ),
),
);
$response = $this->orders_endpoint->confirm_payment_source( $request_body, $body->id );
$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();
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
return array(
'result' => 'success',
'redirect' => esc_url( $payer_action ),
);
} catch ( RuntimeException $exception ) {
$wc_order->update_status(
'failed',
$exception->getMessage()
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
}
/**
* 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
/**
* Multibanco payment method.
*
* @package WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
/**
* Class MultibancoPaymentMethod
*/
class MultibancoPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* Multibanco WC gateway.
*
* @var MultibancoGateway
*/
private $gateway;
/**
* MultibancoPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param MultibancoGateway $gateway Multibanco WC gateway.
*/
public function __construct(
string $module_url,
string $version,
MultibancoGateway $gateway
) {
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->name = MultibancoGateway::ID;
}
/**
* {@inheritDoc}
*/
public function initialize() {}
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-multibanco-payment-method',
trailingslashit( $this->module_url ) . 'assets/js/multibanco-payment-method.js',
array(),
$this->version,
true
);
return array( 'ppcp-multibanco-payment-method' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'icon' => 'multibanco',
);
}
}

View file

@ -13,6 +13,7 @@ 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -72,7 +73,7 @@ class MyBankGateway extends WC_Payment_Gateway {
'products',
);
$this->method_title = __( 'MyBank', 'woocommerce-paypal-payments' );
$this->method_title = __( 'MyBank (via PayPal)', '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' ) );
@ -177,6 +178,9 @@ class MyBankGateway extends WC_Payment_Gateway {
$body = json_decode( $response['body'] );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {

View file

@ -91,7 +91,7 @@ class MyBankPaymentMethod extends AbstractPaymentMethodType {
'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' ),
'icon' => 'mybank',
);
}
}

View file

@ -13,6 +13,7 @@ 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -72,7 +73,7 @@ class P24Gateway extends WC_Payment_Gateway {
'products',
);
$this->method_title = __( 'Przelewy24', 'woocommerce-paypal-payments' );
$this->method_title = __( 'Przelewy24 (via PayPal)', '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' ) );
@ -178,6 +179,9 @@ class P24Gateway extends WC_Payment_Gateway {
$body = json_decode( $response['body'] );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {

View file

@ -91,7 +91,7 @@ class P24PaymentMethod extends AbstractPaymentMethodType {
'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' ),
'icon' => 'p24',
);
}
}

View file

@ -13,6 +13,7 @@ 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\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -72,7 +73,7 @@ class TrustlyGateway extends WC_Payment_Gateway {
'products',
);
$this->method_title = __( 'Trustly', 'woocommerce-paypal-payments' );
$this->method_title = __( 'Trustly (via PayPal)', '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' ) );
@ -177,6 +178,9 @@ class TrustlyGateway extends WC_Payment_Gateway {
$body = json_decode( $response['body'] );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $body->id );
$wc_order->save_meta_data();
$payer_action = '';
foreach ( $body->links as $link ) {
if ( $link->rel === 'payer-action' ) {

View file

@ -91,7 +91,7 @@ class TrustlyPaymentMethod extends AbstractPaymentMethodType {
'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' ),
'icon' => 'trustly',
);
}
}

View file

@ -30,6 +30,9 @@ module.exports = {
'trustly-payment-method': path.resolve(
'./resources/js/trustly-payment-method.js'
),
'multibanco-payment-method': path.resolve(
'./resources/js/multibanco-payment-method.js'
),
},
output: {
path: path.resolve( __dirname, 'assets/' ),

View file

@ -13,6 +13,8 @@ use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'paylater-configurator.url' => static function ( ContainerInterface $container ): string {
@ -44,4 +46,34 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'paylater-configurator.is-available' => static function ( ContainerInterface $container ) : bool {
// Test, if Pay-Later is available; depends on the shop country and Vaulting status.
$messages_apply = $container->get( 'button.helper.messages-apply' );
assert( $messages_apply instanceof MessagesApply );
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$vault_enabled = $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' );
return ! $vault_enabled && $messages_apply->for_country();
},
'paylater-configurator.messaging-locations' => static function ( ContainerInterface $container ) : array {
// Get an array of locations that display the Pay-Later message.
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$is_enabled = $settings->has( 'pay_later_messaging_enabled' ) && $settings->get( 'pay_later_messaging_enabled' );
if ( ! $is_enabled ) {
return array();
}
$selected_locations = $settings->has( 'pay_later_messaging_locations' ) ? $settings->get( 'pay_later_messaging_locations' ) : array();
if ( is_array( $selected_locations ) ) {
return $selected_locations;
}
return array();
},
);

View file

@ -9,7 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterConfigurator;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\GetConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
@ -19,6 +18,8 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameI
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class PayLaterConfiguratorModule
@ -54,19 +55,22 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
$messages_apply = $c->get( 'button.helper.messages-apply' );
assert( $messages_apply instanceof MessagesApply );
public function run( ContainerInterface $c ) : bool {
$is_available = $c->get( 'paylater-configurator.is-available' );
if ( ! $is_available ) {
return true;
}
$current_page_id = $c->get( 'wcgateway.current-ppcp-settings-page-id' );
$is_wc_settings_page = $c->get( 'wcgateway.is-wc-settings-page' );
$messaging_locations = $c->get( 'paylater-configurator.messaging-locations' );
$this->add_paylater_update_notice( $messaging_locations, $is_wc_settings_page, $current_page_id );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$vault_enabled = $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' );
if ( $vault_enabled || ! $messages_apply->for_country() ) {
return true;
}
add_action(
'wc_ajax_' . SaveConfig::ENDPOINT,
static function () use ( $c ) {
@ -85,8 +89,6 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
}
);
$current_page_id = $c->get( 'wcgateway.current-ppcp-settings-page-id' );
if ( $current_page_id !== Settings::PAY_LATER_TAB_ID ) {
return true;
}
@ -150,4 +152,62 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
return true;
}
/**
* Conditionally registers a new admin notice to highlight the new Pay-Later UI.
*
* The notice appears on any PayPal-Settings page, except for the Pay-Later settings page,
* when no Pay-Later messaging is used yet.
*
* @param array $message_locations PayLater messaging locations.
* @param bool $is_settings_page Whether the current page is a WC settings page.
* @param string $current_page_id ID of current settings page tab.
*
* @return void
*/
private function add_paylater_update_notice( array $message_locations, bool $is_settings_page, string $current_page_id ) : void {
// The message must be registered on any WC-Settings page, except for the Pay Later page.
if ( ! $is_settings_page || Settings::PAY_LATER_TAB_ID === $current_page_id ) {
return;
}
// Don't display the notice when Pay-Later messaging is already used.
if ( count( $message_locations ) ) {
return;
}
add_filter(
Repository::NOTICES_FILTER,
/**
* Notify the user about the new Pay-Later UI.
*
* @param array $notices The notices.
* @return array
*
* @psalm-suppress MissingClosureParamType
*/
static function ( $notices ) : array {
$settings_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-pay-later' );
$message = sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag directing to the Pay-Later settings page.
__(
'<strong>NEW</strong>: Check out the recently revamped %1$sPayPal Pay Later messaging experience here%2$s. Get paid in full at checkout while giving your customers the flexibility to pay in installments over time.',
'woocommerce-paypal-payments'
),
'<a href="' . esc_url( $settings_url ) . '">',
'</a>'
);
$notices[] = new PersistentMessage(
'pay-later-messaging',
$message,
'info',
'ppcp-notice-wrapper'
);
return $notices;
}
);
}
}

View file

@ -27,6 +27,7 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Helper\FeesUpdater;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
@ -172,10 +173,14 @@ return array(
return new DisableGateways( $session_handler, $settings, $settings_status, $subscription_helper );
},
'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool {
'wcgateway.is-wc-settings-page' => static function ( ContainerInterface $container ): bool {
$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
return 'wc-settings' === $page;
},
'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool {
$is_wc_settings_page = $container->get( 'wcgateway.is-wc-settings-page' );
$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : '';
return 'wc-settings' === $page && 'checkout' === $tab;
return $is_wc_settings_page && 'checkout' === $tab;
},
'wcgateway.is-wc-gateways-list-page' => static function ( ContainerInterface $container ): bool {
return $container->get( 'wcgateway.is-wc-payments-page' ) && ! isset( $_GET['section'] );
@ -776,6 +781,20 @@ return array(
'requirements' => array(),
'gateway' => 'paypal',
),
'allow_local_apm_gateways' => array(
'title' => __( 'Create gateway for alternative payment methods', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => __( 'Moves the alternative payment methods from the PayPal gateway into their own dedicated gateways.', 'woocommerce-paypal-payments' ),
'description' => __( 'By default, alternative payment methods are displayed in the Standard Payments payment gateway. This setting creates a gateway for each alternative payment method.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'disable_cards' => array(
'title' => __( 'Disable specific credit cards', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-multiselect',
@ -1165,6 +1184,13 @@ return array(
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new RefundFeesUpdater( $order_endpoint, $logger );
},
'wcgateway.helper.fees-updater' => static function ( ContainerInterface $container ): FeesUpdater {
return new FeesUpdater(
$container->get( 'api.endpoint.orders' ),
$container->get( 'api.factory.capture' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers {
return new MessagesDisclaimers(

View file

@ -195,7 +195,7 @@ class PayUponInvoice {
);
add_action(
'ppcp_payment_capture_completed_webhook_handler',
'woocommerce_paypal_payments_payment_capture_completed_webhook_handler',
function ( WC_Order $wc_order, string $order_id ) {
try {
if ( $wc_order->get_payment_method() !== PayUponInvoiceGateway::ID ) {

View file

@ -0,0 +1,98 @@
<?php
/**
* The FeesUpdater helper.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Helper;
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class FeesUpdater
*/
class FeesUpdater {
/**
* The orders' endpoint.
*
* @var Orders
*/
private $orders_endpoint;
/**
* The capture factory.
*
* @var CaptureFactory
*/
private $capture_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* FeesUpdater constructor.
*
* @param Orders $orders_endpoint The orders' endpoint.
* @param CaptureFactory $capture_factory The capture factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
Orders $orders_endpoint,
CaptureFactory $capture_factory,
LoggerInterface $logger
) {
$this->orders_endpoint = $orders_endpoint;
$this->capture_factory = $capture_factory;
$this->logger = $logger;
}
/**
* Updates the fees meta for a given order.
*
* @param string $order_id PayPal order ID.
* @param WC_Order $wc_order WC order.
* @return void
*/
public function update( string $order_id, WC_Order $wc_order ): void {
try {
$order = $this->orders_endpoint->order( $order_id );
} catch ( RuntimeException $exception ) {
$this->logger->warning(
sprintf(
'Could not get PayPal order %1$s when trying to update fees for WC order #%2$s',
$order_id,
$wc_order->get_id()
)
);
return;
}
$body = json_decode( $order['body'] );
$capture = $this->capture_factory->from_paypal_response( $body->purchase_units[0]->payments->captures[0] );
$breakdown = $capture->seller_receivable_breakdown();
if ( $breakdown ) {
$wc_order->update_meta_data( PayPalGateway::FEES_META_KEY, $breakdown->to_array() );
$paypal_fee = $breakdown->paypal_fee();
if ( $paypal_fee ) {
$wc_order->update_meta_data( 'PayPal Transaction Fee', (string) $paypal_fee->value() );
}
$wc_order->save_meta_data();
}
}
}

View file

@ -35,9 +35,25 @@ trait PaymentsStatusHandlingTrait {
WC_Order $wc_order
): void {
if ( $order->intent() === 'CAPTURE' ) {
$this->handle_capture_status( $order->purchase_units()[0]->payments()->captures()[0], $wc_order );
$purchase_units = $order->purchase_units();
if ( ! empty( $purchase_units ) && isset( $purchase_units[0] ) ) {
$payments = $purchase_units[0]->payments();
if ( $payments && ! empty( $payments->captures() ) ) {
$this->handle_capture_status( $payments->captures()[0], $wc_order );
}
}
} elseif ( $order->intent() === 'AUTHORIZE' ) {
$this->handle_authorization_status( $order->purchase_units()[0]->payments()->authorizations()[0], $wc_order );
$purchase_units = $order->purchase_units();
if ( ! empty( $purchase_units ) && isset( $purchase_units[0] ) ) {
$payments = $purchase_units[0]->payments();
if ( $payments && ! empty( $payments->authorizations() ) ) {
$this->handle_authorization_status( $payments->authorizations()[0], $wc_order );
}
}
}
}

View file

@ -109,7 +109,11 @@ class RefundProcessor {
*/
public function process( WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool {
try {
if ( ! in_array( $wc_order->get_payment_method(), array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID, PayUponInvoiceGateway::ID ), true ) ) {
$allowed_refund_payment_methods = apply_filters(
'woocommerce_paypal_payments_allowed_refund_payment_methods',
array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID, PayUponInvoiceGateway::ID )
);
if ( ! in_array( $wc_order->get_payment_method(), $allowed_refund_payment_methods, true ) ) {
return true;
}

View file

@ -505,6 +505,19 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
}
);
add_action(
'woocommerce_paypal_payments_gateway_migrate',
function( string $installed_plugin_version ) use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( ! $installed_plugin_version ) {
$settings->set( 'allow_local_apm_gateways', true );
$settings->persist();
}
}
);
return true;
}

View file

@ -105,7 +105,7 @@ class PaymentCaptureCompleted implements RequestHandler {
/**
* Allow access to the webhook logic before updating the WC order.
*/
do_action( 'ppcp_payment_capture_completed_webhook_handler', $wc_order, $order_id );
do_action( 'woocommerce_paypal_payments_payment_capture_completed_webhook_handler', $wc_order, $order_id );
if ( $wc_order->get_status() !== 'on-hold' ) {
return $this->success_response();

View file

@ -1,12 +1,13 @@
{
"name": "woocommerce-paypal-payments",
"version": "2.8.3",
"version": "2.9.0",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",
"author": "WooCommerce",
"scripts": {
"postinstall": "run-s install:modules:* && run-s build:modules",
"install:modules:ppcp-admin-notices": "cd modules/ppcp-admin-notices && yarn install",
"install:modules:ppcp-applepay": "cd modules/ppcp-applepay && yarn install",
"install:modules:ppcp-blocks": "cd modules/ppcp-blocks && yarn install",
"install:modules:ppcp-paylater-block": "cd modules/ppcp-paylater-block && yarn install",
@ -25,6 +26,7 @@
"install:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
"install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install",
"build:modules:ppcp-admin-notices": "cd modules/ppcp-admin-notices && yarn run build",
"build:modules:ppcp-applepay": "cd modules/ppcp-applepay && yarn run build",
"build:modules:ppcp-blocks": "cd modules/ppcp-blocks && yarn run build",
"build:modules:ppcp-paylater-block": "cd modules/ppcp-paylater-block && yarn run build",
@ -44,6 +46,7 @@
"build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build",
"build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build",
"build:modules": "run-p build:modules:*",
"watch:modules:ppcp-admin-notices": "cd modules/ppcp-admin-notices && yarn run watch",
"watch:modules:ppcp-applepay": "cd modules/ppcp-applepay && yarn run watch",
"watch:modules:ppcp-blocks": "cd modules/ppcp-blocks && yarn run watch",
"watch:modules:ppcp-paylater-block": "cd modules/ppcp-paylater-block && yarn run watch",

View file

@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, checkout, cart, pay later, apple
Requires at least: 5.3
Tested up to: 6.6
Requires PHP: 7.2
Stable tag: 2.8.3
Stable tag: 2.9.0
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -179,6 +179,26 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog ==
= 2.9.0 - xxxx-xx-xx =
* Fix - Fatal error in Block Editor when using WooCommerce blocks #2534
* Fix - Can't pay from block pages when the shipping callback is enabled and no shipping methods defined #2429
* Fix - Various Google Pay button fixes #2496
* Fix - Buying a free trial subscription with ACDC results in a $1 charge in the API call #2465
* Fix - Problem with Google Pay and Apple Pay button placement on Pay for Order page #2542
* Fix - When there isn't any shipping option for the address the order is still created from classic cart #2437
* Fix - Patch the order with no shipping methods, instead of throwing an error #2435
* Enhancement - Separate Apple Pay button for Classic Checkout #2457
* Enhancement - Remove AMEX support for ACDC when store location is set to China #2526
* Enhancement - Inform users of Pay Later messaging configuration when Pay Later wasn't recently enabled #2529
* Enhancement - Update ACDC signup URLs #2475
* Enhancement - Implement country based APMs via Orders API #2511
* Enhancement - Update PaymentsStatusHandlingTrait.php (author @callmeahmedr) #2523
* Enhancement - Disable PayPal Shipping callback by default #2527
* Enhancement - Change Apple Pay and Google Pay default button labels to plain #2476
* Enhancement - Add Package Tracking compatibility with DHL Shipping plugin #2463
* Enhancement - Add support for WC Bookings when skipping checkout confirmation #2452
* Enhancement - Remove currencies from country-currency matrix in card fields module #2441
= 2.8.3 - 2024-08-12 =
* Fix - Google Pay: Prevent field validation from being triggered on checkout page load #2474
* Fix - Do not add tax info into order meta during order creation #2471

View file

@ -3,14 +3,14 @@
* Plugin Name: WooCommerce PayPal Payments
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
* Version: 2.8.3
* Version: 2.9.0
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* License: GPL-2.0
* Requires PHP: 7.2
* Requires Plugins: woocommerce
* WC requires at least: 3.9
* WC tested up to: 9.1
* WC tested up to: 9.2
* Text Domain: woocommerce-paypal-payments
*
* @package WooCommerce\PayPalCommerce
@ -26,7 +26,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' );
define( 'PAYPAL_URL', 'https://www.paypal.com' );
define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' );
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-08-07' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-08-28' );
define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );
@ -106,7 +106,7 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
/**
* The hook fired when the plugin is installed or updated.
*/
do_action( 'woocommerce_paypal_payments_gateway_migrate' );
do_action( 'woocommerce_paypal_payments_gateway_migrate', $installed_plugin_version );
if ( $installed_plugin_version ) {
/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 398 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB