Merge branch 'trunk' into PCP-3283-add-notice-to-migrate-to-checkout-blocks

This commit is contained in:
Philipp Stracker 2024-09-06 15:59:36 +02:00 committed by GitHub
commit 50c5dd6c9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
321 changed files with 22124 additions and 5651 deletions

View file

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

View file

@ -4,7 +4,7 @@
"description": "Admin notices module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): AdminNotices {
return new AdminNotices();
};

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.renderer' => static function ( ContainerInterface $container ): RendererInterface {
$repository = $container->get( 'admin-notices.repository' );
return new Renderer( $repository );
'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.repository' => static function ( ContainerInterface $container ): RepositoryInterface {
'admin-notices.renderer' => static function ( ContainerInterface $container ): RendererInterface {
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,36 +9,46 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
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\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
*/
class AdminNotices implements ModuleInterface {
class AdminNotices implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@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();
}
);
@ -70,13 +80,36 @@ class AdminNotices implements ModuleInterface {
return $notices;
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
/**
* 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.
@ -45,15 +45,15 @@ class Message {
/**
* Message constructor.
*
* @param string $message The message text.
* @param string $type The message type.
* @param bool $dismissable Whether the message is dismissable.
* @param string $wrapper The wrapper selector that will contain the notice.
* @param string $message The message text.
* @param string $type The message type.
* @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

@ -4,7 +4,7 @@
"description": "API client module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return function (): ModuleInterface {
return function (): ApiModule {
return new ApiModule();
};

View file

@ -9,8 +9,10 @@ declare(strict_types=1);
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;
@ -239,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' ),
@ -1655,18 +1664,27 @@ return array(
return new PurchaseUnitSanitizer( $behavior, $line_name );
}
),
'api.client-credentials' => static function( ContainerInterface $container ): ClientCredentials {
return new ClientCredentials(
$container->get( 'wcgateway.settings' )
);
},
'api.client-credentials-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-client-credentials-cache' );
},
'api.user-id-token' => static function( ContainerInterface $container ): UserIdToken {
return new UserIdToken(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.client-credentials' )
);
},
'api.sdk-client-token' => static function( ContainerInterface $container ): SdkClientToken {
return new SdkClientToken(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.client-credentials' ),
$container->get( 'api.client-credentials-cache' )
);
},
);

View file

@ -10,33 +10,41 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
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\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\ApiClient\Entity\Order;
/**
* Class ApiModule
*/
class ApiModule implements ModuleInterface {
class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
add_action(
'woocommerce_after_calculate_totals',
function ( \WC_Cart $cart ) {
@ -94,13 +102,7 @@ class ApiModule implements ModuleInterface {
10,
2
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
return true;
}
}

View file

@ -0,0 +1,49 @@
<?php
/**
* The client credentials.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Authentication
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Authentication;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
/**
* Class ClientCredentials
*/
class ClientCredentials {
/**
* The settings.
*
* @var Settings
*/
protected $settings;
/**
* ClientCredentials constructor.
*
* @param Settings $settings The settings.
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* Returns encoded client credentials.
*
* @return string
* @throws NotFoundException If setting does not found.
*/
public function credentials(): string {
$client_id = $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : '';
$client_secret = $this->settings->has( 'client_secret' ) ? $this->settings->get( 'client_secret' ) : '';
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return 'Basic ' . base64_encode( $client_id . ':' . $client_secret );
}
}

View file

@ -11,6 +11,7 @@ use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WP_Error;
/**
@ -20,6 +21,8 @@ class SdkClientToken {
use RequestTrait;
const CACHE_KEY = 'sdk-client-token-key';
/**
* The host.
*
@ -27,13 +30,6 @@ class SdkClientToken {
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
@ -41,35 +37,52 @@ class SdkClientToken {
*/
private $logger;
/**
* The client credentials.
*
* @var ClientCredentials
*/
private $client_credentials;
/**
* The cache.
*
* @var Cache
*/
private $cache;
/**
* SdkClientToken constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
* @param string $host The host.
* @param LoggerInterface $logger The logger.
* @param ClientCredentials $client_credentials The client credentials.
* @param Cache $cache The cache.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
LoggerInterface $logger,
ClientCredentials $client_credentials,
Cache $cache
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
$this->host = $host;
$this->logger = $logger;
$this->client_credentials = $client_credentials;
$this->cache = $cache;
}
/**
* Returns `sdk_client_token` which uniquely identifies the payer.
*
* @param string $target_customer_id Vaulted customer id.
* Returns the client token for SDK `data-sdk-client-token`.
*
* @return string
*
* @throws PayPalApiException If the request fails.
* @throws RuntimeException If something unexpected happens.
*/
public function sdk_client_token( string $target_customer_id = '' ): string {
$bearer = $this->bearer->bearer();
public function sdk_client_token(): string {
if ( $this->cache->has( self::CACHE_KEY ) ) {
return $this->cache->get( self::CACHE_KEY );
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$domain = wp_unslash( $_SERVER['HTTP_HOST'] ?? '' );
@ -77,19 +90,10 @@ class SdkClientToken {
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=client_token&intent=sdk_init&domains[]=' . $domain;
if ( $target_customer_id ) {
$url = add_query_arg(
array(
'target_customer_id' => $target_customer_id,
),
$url
);
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Authorization' => $this->client_credentials->credentials(),
'Content-Type' => 'application/x-www-form-urlencoded',
),
);
@ -105,6 +109,11 @@ class SdkClientToken {
throw new PayPalApiException( $json, $status_code );
}
return $json->access_token;
$access_token = $json->access_token;
$expires_in = (int) $json->expires_in;
$this->cache->set( self::CACHE_KEY, $access_token, $expires_in );
return $access_token;
}
}

View file

@ -27,13 +27,6 @@ class UserIdToken {
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
@ -41,21 +34,28 @@ class UserIdToken {
*/
private $logger;
/**
* The client credentials.
*
* @var ClientCredentials
*/
private $client_credentials;
/**
* UserIdToken constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
* @param string $host The host.
* @param LoggerInterface $logger The logger.
* @param ClientCredentials $client_credentials The client credentials.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
LoggerInterface $logger,
ClientCredentials $client_credentials
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
$this->host = $host;
$this->logger = $logger;
$this->client_credentials = $client_credentials;
}
/**
@ -69,8 +69,6 @@ class UserIdToken {
* @throws RuntimeException If something unexpected happens.
*/
public function id_token( string $target_customer_id = '' ): string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=id_token';
if ( $target_customer_id ) {
$url = add_query_arg(
@ -84,7 +82,7 @@ class UserIdToken {
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Authorization' => $this->client_credentials->credentials(),
'Content-Type' => 'application/x-www-form-urlencoded',
),
);

View file

@ -0,0 +1,206 @@
<?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 ( ! 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(
$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
);
}
return $response;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="78.102px" height="50px" viewBox="-5 -5 175.52107 115.9651" enable-background="new 0 0 165.52107 105.9651"
xml:space="preserve">
<g>
<path id="XMLID_4_" d="M150.69807,0H14.82318c-0.5659,0-1.1328,0-1.69769,0.0033c-0.47751,0.0034-0.95391,0.0087-1.43031,0.0217
c-1.039,0.0281-2.0869,0.0894-3.1129,0.2738c-1.0424,0.1876-2.0124,0.4936-2.9587,0.9754
c-0.9303,0.4731-1.782,1.0919-2.52009,1.8303c-0.73841,0.7384-1.35721,1.5887-1.83021,2.52
c-0.4819,0.9463-0.7881,1.9166-0.9744,2.9598c-0.18539,1.0263-0.2471,2.074-0.2751,3.1119
c-0.0128,0.4764-0.01829,0.9528-0.0214,1.4291c-0.0033,0.5661-0.0022,1.1318-0.0022,1.6989V91.142
c0,0.5671-0.0011,1.13181,0.0022,1.69901c0.00311,0.4763,0.0086,0.9527,0.0214,1.4291
c0.028,1.03699,0.08971,2.08469,0.2751,3.11069c0.1863,1.0436,0.4925,2.0135,0.9744,2.9599
c0.473,0.9313,1.0918,1.7827,1.83021,2.52c0.73809,0.7396,1.58979,1.3583,2.52009,1.8302
c0.9463,0.4831,1.9163,0.7892,2.9587,0.9767c1.026,0.1832,2.0739,0.2456,3.1129,0.2737c0.4764,0.0108,0.9528,0.0172,1.43031,0.0194
c0.56489,0.0044,1.13179,0.0044,1.69769,0.0044h135.87489c0.5649,0,1.13181,0,1.69659-0.0044
c0.47641-0.0022,0.95282-0.0086,1.4314-0.0194c1.0368-0.0281,2.0845-0.0905,3.11301-0.2737
c1.041-0.1875,2.0112-0.4936,2.9576-0.9767c0.9313-0.4719,1.7805-1.0906,2.52011-1.8302c0.7372-0.7373,1.35599-1.5887,1.8302-2.52
c0.48299-0.9464,0.78889-1.9163,0.97429-2.9599c0.1855-1.026,0.2457-2.0737,0.2738-3.11069
c0.013-0.4764,0.01941-0.9528,0.02161-1.4291c0.00439-0.5672,0.00439-1.1319,0.00439-1.69901V14.8242
c0-0.5671,0-1.1328-0.00439-1.6989c-0.0022-0.4763-0.00861-0.9527-0.02161-1.4291c-0.02811-1.0379-0.0883-2.0856-0.2738-3.1119
c-0.18539-1.0432-0.4913-2.0135-0.97429-2.9598c-0.47421-0.9313-1.093-1.7816-1.8302-2.52
c-0.73961-0.7384-1.58881-1.3572-2.52011-1.8303c-0.9464-0.4818-1.9166-0.7878-2.9576-0.9754
c-1.0285-0.1844-2.0762-0.2457-3.11301-0.2738c-0.47858-0.013-0.95499-0.0183-1.4314-0.0217C151.82988,0,151.26297,0,150.69807,0
L150.69807,0z"/>
<path id="XMLID_3_" fill="#FFFFFF" d="M150.69807,3.532l1.67149,0.0032c0.4528,0.0032,0.90561,0.0081,1.36092,0.0205
c0.79201,0.0214,1.71849,0.0643,2.58209,0.2191c0.7507,0.1352,1.38029,0.3408,1.9845,0.6484
c0.5965,0.3031,1.14301,0.7003,1.62019,1.1768c0.479,0.4797,0.87671,1.0271,1.18381,1.6302
c0.30589,0.5995,0.51019,1.2261,0.64459,1.9823c0.1544,0.8542,0.1971,1.7832,0.21881,2.5801
c0.01219,0.4498,0.01819,0.8996,0.0204,1.3601c0.00429,0.5569,0.0042,1.1135,0.0042,1.6715V91.142
c0,0.558,0.00009,1.1136-0.0043,1.6824c-0.00211,0.4497-0.0081,0.8995-0.0204,1.3501c-0.02161,0.7957-0.0643,1.7242-0.2206,2.5885
c-0.13251,0.7458-0.3367,1.3725-0.64429,1.975c-0.30621,0.6016-0.70331,1.1484-1.18022,1.6251
c-0.47989,0.48-1.0246,0.876-1.62819,1.1819c-0.5997,0.3061-1.22821,0.51151-1.97151,0.6453
c-0.88109,0.157-1.84639,0.2002-2.57339,0.2199c-0.4574,0.0103-0.9126,0.01649-1.37889,0.0187
c-0.55571,0.0043-1.1134,0.0042-1.6692,0.0042H14.82318c-0.0074,0-0.0146,0-0.0221,0c-0.5494,0-1.0999,0-1.6593-0.0043
c-0.4561-0.00211-0.9112-0.0082-1.3512-0.0182c-0.7436-0.0201-1.7095-0.0632-2.5834-0.2193
c-0.74969-0.1348-1.3782-0.3402-1.9858-0.6503c-0.59789-0.3032-1.1422-0.6988-1.6223-1.1797
c-0.4764-0.4756-0.8723-1.0207-1.1784-1.6232c-0.3064-0.6019-0.5114-1.2305-0.64619-1.9852
c-0.15581-0.8626-0.19861-1.7874-0.22-2.5777c-0.01221-0.4525-0.01731-0.9049-0.02021-1.3547l-0.0022-1.3279l0.0001-0.3506V14.8242
l-0.0001-0.3506l0.0021-1.3251c0.003-0.4525,0.0081-0.9049,0.02031-1.357c0.02139-0.7911,0.06419-1.7163,0.22129-2.5861
c0.1336-0.7479,0.3385-1.3765,0.6465-1.9814c0.3037-0.5979,0.7003-1.1437,1.17921-1.6225
c0.477-0.4772,1.02309-0.8739,1.62479-1.1799c0.6011-0.3061,1.2308-0.5116,1.9805-0.6465c0.8638-0.1552,1.7909-0.198,2.5849-0.2195
c0.4526-0.0123,0.9052-0.0172,1.3544-0.0203l1.6771-0.0033H150.69807"/>
<g>
<g>
<path d="M45.1862,35.64053c1.41724-1.77266,2.37897-4.15282,2.12532-6.58506c-2.07464,0.10316-4.60634,1.36871-6.07207,3.14276
c-1.31607,1.5192-2.4809,3.99902-2.17723,6.3293C41.39111,38.72954,43.71785,37.36345,45.1862,35.64053"/>
<path d="M47.28506,38.98252c-3.38211-0.20146-6.25773,1.91951-7.87286,1.91951c-1.61602,0-4.08931-1.81799-6.76438-1.76899
c-3.48177,0.05114-6.71245,2.01976-8.4793,5.15079c-3.63411,6.2636-0.95904,15.55471,2.57494,20.65606
c1.71618,2.5238,3.78447,5.30269,6.50976,5.20287c2.57494-0.10104,3.58421-1.66732,6.71416-1.66732
c3.12765,0,4.03679,1.66732,6.76252,1.61681c2.82665-0.05054,4.59381-2.52506,6.30997-5.05132
c1.96878-2.877,2.77473-5.65498,2.82542-5.80748c-0.0507-0.05051-5.45058-2.12204-5.50065-8.33358
c-0.05098-5.20101,4.23951-7.6749,4.44144-7.82832C52.3832,39.4881,48.5975,39.08404,47.28506,38.98252"/>
</g>
<g>
<path d="M76.73385,31.94381c7.35096,0,12.4697,5.06708,12.4697,12.44437c0,7.40363-5.22407,12.49704-12.65403,12.49704h-8.13892
v12.94318h-5.88037v-37.8846H76.73385z M68.41059,51.9493h6.74732c5.11975,0,8.0336-2.75636,8.0336-7.53479
c0-4.77792-2.91385-7.50845-8.00727-7.50845h-6.77365V51.9493z"/>
<path d="M90.73997,61.97864c0-4.8311,3.70182-7.79761,10.26583-8.16526l7.56061-0.44614v-2.12639
c0-3.07185-2.07423-4.90959-5.53905-4.90959c-3.28251,0-5.33041,1.57492-5.82871,4.04313h-5.35574
c0.31499-4.98859,4.56777-8.66407,11.3941-8.66407c6.69466,0,10.97377,3.54432,10.97377,9.08388v19.03421h-5.43472v-4.54194
h-0.13065c-1.60125,3.07185-5.09341,5.01441-8.71623,5.01441C94.52078,70.30088,90.73997,66.94038,90.73997,61.97864z
M108.56641,59.4846v-2.17905l-6.8,0.41981c-3.38683,0.23649-5.30306,1.73291-5.30306,4.09579
c0,2.41504,1.99523,3.99046,5.04075,3.99046C105.46823,65.81161,108.56641,63.08108,108.56641,59.4846z"/>
<path d="M119.34167,79.9889v-4.5946c0.4193,0.10483,1.36425,0.10483,1.83723,0.10483c2.6252,0,4.04313-1.10245,4.90908-3.9378
c0-0.05267,0.49931-1.68025,0.49931-1.70658l-9.97616-27.64562h6.14268l6.98432,22.47371h0.10432l6.98433-22.47371h5.9857
l-10.34483,29.06304c-2.36186,6.69517-5.0924,8.84789-10.81577,8.84789C121.17891,80.12006,119.76098,80.06739,119.34167,79.9889
z"/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -4,7 +4,7 @@
"description": "Applepay module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
// Used in various places to mark fields for the preview button.
$apm_name = 'ApplePay';
@ -101,7 +101,7 @@ return array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/applepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/applepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Apple Pay', 'woocommerce-paypal-payments' )
),
@ -155,7 +155,7 @@ return array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/applepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/applepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Apple Pay', 'woocommerce-paypal-payments' )
),
@ -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

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Applepay;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): ApplepayModule {
return new ApplepayModule();
};

View file

@ -23,6 +23,11 @@
&.ppcp-button-minicart {
--apple-pay-button-display: block;
}
&.ppcp-preview-button.ppcp-button-dummy {
/* URL must specify the correct module-folder! */
--apm-button-dummy-background: url(../../../ppcp-applepay/assets/images/applepay.png);
}
}
.wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {
@ -52,3 +57,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,97 @@ 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.
*
* TODO - extend from PaymentButton (same as we do in GooglepayButton.js)
*/
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 +108,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 +335,41 @@ 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.isSeparateGateway ) {
document
.querySelectorAll( '#ppc-button-applepay-container' )
.forEach( ( el ) => el.remove() );
}
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 +378,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 +432,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 +452,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 +467,7 @@ class ApplepayButton {
session.onshippingcontactselected =
this.onShippingContactSelected( session );
}
session.onvalidatemerchant = this.onValidateMerchant( session );
session.onpaymentauthorized = this.onPaymentAuthorized( session );
return session;
@ -199,32 +475,39 @@ 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.remove( 'ppcp-button-rect', 'ppcp-button-pill' );
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 +518,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 +554,7 @@ class ApplepayButton {
PayPalCommerceGateway.ajax.validate_checkout.nonce
)
: null;
if ( formValidator ) {
try {
const errors = await formValidator.validate(
@ -296,13 +582,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 +596,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 +609,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 +643,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 +711,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 +740,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 +752,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 +791,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 +808,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 +830,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 +859,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 +867,7 @@ class ApplepayButton {
const data = this.getShippingContactData( event );
jQuery.ajax( {
url: ajax_url,
url: ajaxUrl,
method: 'POST',
data,
success: (
@ -603,15 +897,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 +913,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,37 +930,42 @@ 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,
simplified_contact:
this.updatedContactInfo ||
this.initialPaymentRequest.shippingContact ||
this.initialPaymentRequest.billingContact,
product_id,
simplified_contact: this.hasValidContactInfo(
this.updatedContactInfo
)
? this.updatedContactInfo
: this.initialPaymentRequest?.shippingContact ??
this.initialPaymentRequest?.billingContact,
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,
simplified_contact:
this.updatedContactInfo ||
this.initialPaymentRequest.shippingContact ||
this.initialPaymentRequest.billingContact,
simplified_contact: this.hasValidContactInfo(
this.updatedContactInfo
)
? this.updatedContactInfo
: this.initialPaymentRequest?.shippingContact ??
this.initialPaymentRequest?.billingContact,
caller_page: 'cart',
'woocommerce-process-checkout-nonce': this.nonce,
};
@ -677,9 +977,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 {
@ -694,7 +991,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,
@ -719,7 +1016,7 @@ class ApplepayButton {
jQuery.ajax( {
url: this.buttonConfig.ajax_url,
method: 'POST',
data: request_data,
data: requestData,
complete: ( jqXHR, textStatus ) => {
this.log( 'onpaymentauthorized complete' );
},
@ -781,7 +1078,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(
@ -948,6 +1246,12 @@ class ApplepayButton {
return btoa( utf8Str );
}
hasValidContactInfo( value ) {
return Array.isArray( value )
? value.length > 0
: Object.keys( value || {} ).length > 0;
}
}
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,6 +1,5 @@
import ErrorHandler from '../../../../ppcp-button/resources/js/modules/ErrorHandler';
import CartActionHandler from '../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler';
import { isPayPalSubscription } from '../../../../ppcp-blocks/resources/js/Helper/Subscription';
class BaseHandler {
constructor( buttonConfig, ppcpConfig ) {
@ -24,7 +23,7 @@ class BaseHandler {
}
shippingAllowed() {
return this.buttonConfig.product.needsShipping;
return this.buttonConfig.product.needShipping;
}
transactionInfo() {
@ -76,13 +75,6 @@ class BaseHandler {
document.querySelector( '.woocommerce-notices-wrapper' )
);
}
errorHandler() {
return new ErrorHandler(
this.ppcpConfig.labels.error.generic,
document.querySelector( '.woocommerce-notices-wrapper' )
);
}
}
export default BaseHandler;

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

@ -0,0 +1,54 @@
import ApplepayButton from '../ApplepayButton';
import PreviewButton from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButton';
/**
* A single Apple Pay preview button instance.
*/
export default class ApplePayPreviewButton extends PreviewButton {
constructor( args ) {
super( args );
this.selector = `${ args.selector }ApplePay`;
this.defaultAttributes = {
button: {
type: 'pay',
color: 'black',
lang: 'en',
},
};
}
createButton( buttonConfig ) {
const button = new ApplepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig
);
button.init( this.apiConfig );
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
* @param buttonConfig
* @param ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// The Apple Pay button expects the "wrapper" to be an ID without `#` prefix!
buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace(
/^#/,
''
);
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button ) {
buttonConfig.button.type = ppcpConfig.button.style.type;
buttonConfig.button.color = ppcpConfig.button.style.color;
buttonConfig.button.lang =
ppcpConfig.button.style?.lang ||
ppcpConfig.button.style.language;
}
}
}

View file

@ -0,0 +1,50 @@
import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButtonManager';
import ApplePayPreviewButton from './ApplePayPreviewButton';
/**
* Manages all Apple Pay preview buttons on this page.
*/
export default class ApplePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'ApplePay',
buttonConfig: window.wc_ppcp_applepay_admin,
};
super( args );
}
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Applepay()?.config;
if ( ! apiMethod ) {
this.error(
'configuration object cannot be retrieved from PayPal'
);
return {};
}
return await apiMethod();
}
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {ApplePayPreviewButton}
*/
createButtonInstance( wrapperId ) {
return new ApplePayPreviewButton( {
selector: wrapperId,
apiConfig: this.apiConfig,
methodName: this.methodName,
} );
}
}

View file

@ -1,6 +1,4 @@
import ApplepayButton from './ApplepayButton';
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
import ApplePayPreviewButtonManager from './Preview/ApplePayPreviewButtonManager';
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
@ -14,111 +12,5 @@ const buttonManager = () => {
return ApplePayPreviewButtonManager.instance;
};
/**
* Manages all Apple Pay preview buttons on this page.
*/
class ApplePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'ApplePay',
buttonConfig: window.wc_ppcp_applepay_admin,
};
super( args );
}
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Applepay()?.config;
if ( ! apiMethod ) {
this.error(
'configuration object cannot be retrieved from PayPal'
);
return {};
}
return await apiMethod();
}
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {ApplePayPreviewButton}
*/
createButtonInstance( wrapperId ) {
return new ApplePayPreviewButton( {
selector: wrapperId,
apiConfig: this.apiConfig,
} );
}
}
/**
* A single Apple Pay preview button instance.
*/
class ApplePayPreviewButton extends PreviewButton {
constructor( args ) {
super( args );
this.selector = `${ args.selector }ApplePay`;
this.defaultAttributes = {
button: {
type: 'pay',
color: 'black',
lang: 'en',
},
};
}
createNewWrapper() {
const element = super.createNewWrapper();
element.addClass( 'ppcp-button-applepay' );
return element;
}
createButton( buttonConfig ) {
const button = new ApplepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig
);
button.init( this.apiConfig );
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
* @param buttonConfig
* @param ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// The Apple Pay button expects the "wrapper" to be an ID without `#` prefix!
buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace(
/^#/,
''
);
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button ) {
buttonConfig.button.type = ppcpConfig.button.style.type;
buttonConfig.button.color = ppcpConfig.button.style.color;
buttonConfig.button.lang =
ppcpConfig.button.style?.lang ||
ppcpConfig.button.style.language;
}
}
}
// Initialize the preview button manager.
buttonManager();

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

@ -24,32 +24,33 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'applepay.eligible' => static function ( ContainerInterface $container ): bool {
'applepay.eligible' => static function ( ContainerInterface $container ): bool {
$apm_applies = $container->get( 'applepay.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country_currency();
return $apm_applies->for_country() && $apm_applies->for_currency();
},
'applepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
'applepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
$container->get( 'applepay.supported-country-currency-matrix' ),
$container->get( 'applepay.supported-countries' ),
$container->get( 'applepay.supported-currencies' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
'applepay.status-cache' => static function( ContainerInterface $container ): Cache {
'applepay.status-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-apple-status-cache' );
},
// We assume it's a referral if we can check product status without API request failures.
'applepay.is_referral' => static function ( ContainerInterface $container ): bool {
'applepay.is_referral' => static function ( ContainerInterface $container ): bool {
$status = $container->get( 'applepay.apple-product-status' );
assert( $status instanceof AppleProductStatus );
return ! $status->has_request_failure();
},
'applepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
'applepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
$settings = $container->get( 'wcgateway.settings' );
return new AvailabilityNotice(
@ -63,17 +64,17 @@ return array(
);
},
'applepay.has_validated' => static function ( ContainerInterface $container ): bool {
'applepay.has_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' );
},
'applepay.is_validated' => static function ( ContainerInterface $container ): bool {
'applepay.is_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false;
},
'applepay.apple-product-status' => SingletonDecorator::make(
'applepay.apple-product-status' => SingletonDecorator::make(
static function( ContainerInterface $container ): AppleProductStatus {
return new AppleProductStatus(
$container->get( 'wcgateway.settings' ),
@ -83,7 +84,7 @@ return array(
);
}
),
'applepay.available' => static function ( ContainerInterface $container ): bool {
'applepay.available' => static function ( ContainerInterface $container ): bool {
if ( apply_filters( 'woocommerce_paypal_payments_applepay_validate_product_status', true ) ) {
$status = $container->get( 'applepay.apple-product-status' );
assert( $status instanceof AppleProductStatus );
@ -94,10 +95,10 @@ return array(
}
return true;
},
'applepay.server_supported' => static function ( ContainerInterface $container ): bool {
'applepay.server_supported' => static function ( ContainerInterface $container ): bool {
return ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off';
},
'applepay.is_browser_supported' => static function ( ContainerInterface $container ): bool {
'applepay.is_browser_supported' => static function ( ContainerInterface $container ): bool {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$user_agent = wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' );
if ( $user_agent ) {
@ -127,7 +128,7 @@ return array(
}
return false;
},
'applepay.url' => static function ( ContainerInterface $container ): string {
'applepay.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
@ -137,13 +138,13 @@ return array(
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'applepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
'applepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
return 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
},
'applepay.data_to_scripts' => static function ( ContainerInterface $container ): DataToAppleButtonScripts {
'applepay.data_to_scripts' => static function ( ContainerInterface $container ): DataToAppleButtonScripts {
return new DataToAppleButtonScripts( $container->get( 'applepay.sdk_script_url' ), $container->get( 'wcgateway.settings' ) );
},
'applepay.button' => static function ( ContainerInterface $container ): ApplePayButton {
'applepay.button' => static function ( ContainerInterface $container ): ApplePayButton {
return new ApplePayButton(
$container->get( 'wcgateway.settings' ),
$container->get( 'woocommerce.logger.woocommerce' ),
@ -155,7 +156,7 @@ return array(
$container->get( 'button.helper.cart-products' )
);
},
'applepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
'applepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
return new BlocksPaymentMethod(
'ppcp-applepay',
$container->get( 'applepay.url' ),
@ -164,781 +165,103 @@ return array(
$container->get( 'blocks.method' )
);
},
/**
* The matrix which countries and currency combinations can be used for ApplePay.
* The list of which countries can be used for ApplePay.
*/
'applepay.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
'applepay.supported-countries' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for ApplePay.
* Returns which countries can be used for ApplePay.
*/
return apply_filters(
'woocommerce_paypal_payments_applepay_supported_country_currency_matrix',
'woocommerce_paypal_payments_applepay_supported_countries',
// phpcs:disable Squiz.Commenting.InlineComment
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',
),
'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',
),
'NO' => 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',
),
'AU', // Australia
'AT', // Austria
'BE', // Belgium
'BG', // Bulgaria
'CA', // Canada
'CN', // China
'CY', // Cyprus
'CZ', // Czech Republic
'DK', // Denmark
'EE', // Estonia
'FI', // Finland
'FR', // France
'DE', // Germany
'GR', // Greece
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
'LV', // Latvia
'LI', // Liechtenstein
'LT', // Lithuania
'LU', // Luxembourg
'MT', // Malta
'NL', // Netherlands
'NO', // Norway
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SK', // Slovakia
'SI', // Slovenia
'ES', // Spain
'SE', // Sweden
'US', // United States
'GB', // United Kingdom
)
// phpcs:enable Squiz.Commenting.InlineComment
);
},
'applepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
/**
* The list of which currencies can be used for ApplePay.
*/
'applepay.supported-currencies' => static function ( ContainerInterface $container ) : array {
/**
* Returns which currencies can be used for ApplePay.
*/
return apply_filters(
'woocommerce_paypal_payments_applepay_supported_currencies',
// phpcs:disable Squiz.Commenting.InlineComment
array(
'AUD', // Australian Dollar
'BRL', // Brazilian Real
'CAD', // Canadian Dollar
'CHF', // Swiss Franc
'CZK', // Czech Koruna
'DKK', // Danish Krone
'EUR', // Euro
'GBP', // British Pound Sterling
'HKD', // Hong Kong Dollar
'HUF', // Hungarian Forint
'ILS', // Israeli New Shekel
'JPY', // Japanese Yen
'MXN', // Mexican Peso
'NOK', // Norwegian Krone
'NZD', // New Zealand Dollar
'PHP', // Philippine Peso
'PLN', // Polish Zloty
'SEK', // Swedish Krona
'SGD', // Singapore Dollar
'THB', // Thai Baht
'TWD', // New Taiwan Dollar
'USD', // United States Dollar
)
// phpcs:enable Squiz.Commenting.InlineComment
);
},
'applepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY';
},
'applepay.enable-url-live' => static function ( ContainerInterface $container ): string {
'applepay.enable-url-live' => static function ( ContainerInterface $container ): string {
return 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY';
},
'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
@ -976,5 +299,16 @@ 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' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,242 @@
<?php
/**
* The Apple Pay Payment Gateway
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
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;
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;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* 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.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
OrderProcessor $order_processor,
callable $paypal_checkout_url_factory,
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider,
SessionHandler $session_handler,
string $module_url,
LoggerInterface $logger
) {
$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.svg';
$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;
$this->logger = $logger;
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,30 +18,37 @@ 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\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
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\WcGateway\Settings\Settings;
/**
* Class ApplepayModule
*/
class ApplepayModule implements ModuleInterface {
class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
$module = $this;
// Clears product status when appropriate.
@ -117,14 +125,50 @@ class ApplepayModule implements ModuleInterface {
100,
2
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
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;
}
/**
@ -306,7 +350,7 @@ class ApplepayModule implements ModuleInterface {
* @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

@ -16,11 +16,18 @@ namespace WooCommerce\PayPalCommerce\Applepay\Helper;
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for ApplePay.
* The list of which countries can be used for ApplePay.
*
* @var array
*/
private $allowed_country_currency_matrix;
private $allowed_countries;
/**
* The list of which currencies can be used for ApplePay.
*
* @var array
*/
private $allowed_currencies;
/**
* 3-letter currency code of the shop.
@ -37,32 +44,41 @@ class ApmApplies {
private $country;
/**
* ApmApplies constructor.
* DccApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for ApplePay.
* @param array $allowed_countries The list of which countries can be used for ApplePay.
* @param array $allowed_currencies The list of which currencies can be used for ApplePay.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
array $allowed_country_currency_matrix,
array $allowed_countries,
array $allowed_currencies,
string $currency,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
$this->allowed_countries = $allowed_countries;
$this->allowed_currencies = $allowed_currencies;
$this->currency = $currency;
$this->country = $country;
}
/**
* Returns whether ApplePay can be used in the current country and the current currency used.
* Returns whether ApplePay can be used in the current country used.
*
* @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_countries, true );
}
/**
* Returns whether ApplePay can be used in the current currency used.
*
* @return bool
*/
public function for_currency(): bool {
return in_array( $this->currency, $this->allowed_currencies, true );
}
}

View file

@ -4,7 +4,7 @@
"description": "Axo module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -17,7 +17,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
$insert_after = function( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
@ -83,7 +83,7 @@ return array(
->rule()
->condition_element( 'axo_enabled', '1' )
->action_visible( 'axo_gateway_title' )
->action_visible( 'axo_checkout_config_notice' )
->action_visible( 'axo_main_notice' )
->action_visible( 'axo_privacy' )
->action_visible( 'axo_name_on_card' )
->action_visible( 'axo_style_heading' )
@ -114,9 +114,17 @@ return array(
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
'axo_checkout_config_notice' => array(
'axo_main_notice' => array(
'heading' => '',
'html' => $container->get( 'axo.checkout-config-notice' ),
'html' => implode(
'',
array(
$container->get( 'axo.settings-conflict-notice' ),
$container->get( 'axo.shipping-config-notice' ),
$container->get( 'axo.checkout-config-notice' ),
$container->get( 'axo.incompatible-plugins-notice' ),
)
),
'type' => 'ppcp-html',
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): AxoModule {
return new AxoModule();
};

View file

@ -709,6 +709,8 @@ class AxoManager {
}`
);
this.emailInput.value = this.stripSpaces( this.emailInput.value );
this.$( this.el.paymentContainer.selector + '-detail' ).html( '' );
this.$( this.el.paymentContainer.selector + '-form' ).html( '' );
@ -1134,6 +1136,10 @@ class AxoManager {
return emailPattern.test( value );
}
stripSpaces( str ) {
return str.replace( /\s+/g, '' );
}
validateEmail( billingEmail ) {
const billingEmailSelector = document.querySelector( billingEmail );
const value = document.querySelector( billingEmail + ' input' ).value;

View file

@ -1,7 +1,19 @@
export function log( message, level = 'info' ) {
const wpDebug = window.wc_ppcp_axo?.wp_debug;
const endpoint = window.wc_ppcp_axo?.ajax?.frontend_logger?.endpoint;
if ( ! endpoint ) {
const loggingEnabled = window.wc_ppcp_axo?.logging_enabled;
if ( wpDebug ) {
switch ( level ) {
case 'error':
console.error( `[AXO] ${ message }` );
break;
default:
console.log( `[AXO] ${ message }` );
}
}
if ( ! endpoint || ! loggingEnabled ) {
return;
}
@ -15,15 +27,5 @@ export function log( message, level = 'info' ) {
level,
},
} ),
} ).then( () => {
if ( wpDebug ) {
switch ( level ) {
case 'error':
console.error( `[AXO] ${ message }` );
break;
default:
console.log( `[AXO] ${ message }` );
}
}
} );
}

View file

@ -12,10 +12,10 @@ namespace WooCommerce\PayPalCommerce\Axo;
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Axo\Helper\SettingsNoticeGenerator;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
@ -25,7 +25,7 @@ return array(
$apm_applies = $container->get( 'axo.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country_currency() && $apm_applies->for_settings();
return $apm_applies->for_country_currency();
},
'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
@ -36,6 +36,10 @@ return array(
);
},
'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator {
return new SettingsNoticeGenerator();
},
// If AXO is configured and onboarded.
'axo.available' => static function ( ContainerInterface $container ): bool {
return true;
@ -159,48 +163,35 @@ return array(
);
},
'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
return $settings_notice_generator->generate_settings_conflict_notice( $settings );
},
'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string {
$checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
$block_checkout_docs_link = __(
'https://woocommerce.com/document/cart-checkout-blocks-status/#reverting-to-the-cart-and-checkout-shortcodes',
'woocommerce-paypal-payments'
);
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
if ( CartCheckoutDetector::has_elementor_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the <code>Elementor Checkout widget</code>. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
} elseif ( CartCheckoutDetector::has_block_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the WooCommerce <code>Checkout</code> block. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
} elseif ( ! CartCheckoutDetector::has_classic_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store does not seem to be properly configured or uses an incompatible <code>third-party Checkout</code> solution. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
} else {
return '';
}
return $settings_notice_generator->generate_checkout_notice();
},
return '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>';
'axo.shipping-config-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
return $settings_notice_generator->generate_shipping_notice();
},
'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
return $settings_notice_generator->generate_incompatible_plugins_notice();
},
'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
@ -230,6 +221,7 @@ return array(
return '<div class="ppcp-notice ppcp-notice-warning"><p>' . $notice_content . '</p></div>';
},
'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint {
return new FrontendLoggerEndpoint(
$container->get( 'button.request-data' ),

View file

@ -216,6 +216,7 @@ class AxoManager {
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
),
),
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ),
);

View file

@ -17,9 +17,10 @@ use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
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\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\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -29,21 +30,27 @@ use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Class AxoModule
*/
class AxoModule implements ModuleInterface {
class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
add_filter(
'woocommerce_payment_gateways',
@ -66,7 +73,7 @@ class AxoModule implements ModuleInterface {
// Add the gateway in admin area.
if ( is_admin() ) {
$methods[] = $gateway;
// $methods[] = $gateway; - Temporarily remove Fastlane from the payment gateway list in admin area.
return $methods;
}
@ -77,9 +84,10 @@ class AxoModule implements ModuleInterface {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
$is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false;
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
if ( ! $is_dcc_enabled ) {
if ( ! $is_paypal_enabled || ! $is_dcc_enabled ) {
return $methods;
}
@ -87,6 +95,10 @@ class AxoModule implements ModuleInterface {
return $methods;
}
if ( ! $this->is_compatible_shipping_config() ) {
return $methods;
}
$methods[] = $gateway;
return $methods;
},
@ -144,13 +156,20 @@ class AxoModule implements ModuleInterface {
function () use ( $c ) {
$module = $this;
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false;
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
// Check if the module is applicable, correct country, currency, ... etc.
if ( ! $c->get( 'axo.eligible' )
if ( ! $is_paypal_enabled
|| ! $c->get( 'axo.eligible' )
|| 'continuation' === $c->get( 'button.context' )
|| $subscription_helper->cart_contains_subscription() ) {
|| $subscription_helper->cart_contains_subscription()
|| ! $this->is_compatible_shipping_config() ) {
return;
}
@ -194,9 +213,17 @@ class AxoModule implements ModuleInterface {
add_action(
'wp_head',
function () {
function () use ( $c ) {
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
echo '<script async src="https://www.paypalobjects.com/insights/v1/paypal-insights.sandbox.min.js"></script>';
// Add meta tag to allow feature-detection of the site's AXO payment state.
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$this->add_feature_detection_tag(
$settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' )
);
}
);
@ -264,6 +291,7 @@ class AxoModule implements ModuleInterface {
$endpoint->handle_request();
}
);
return true;
}
/**
@ -280,15 +308,7 @@ class AxoModule implements ModuleInterface {
array $localized_script_data
): array {
try {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( ! $target_customer_id ) {
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
}
}
$sdk_client_token = $api->sdk_client_token( $target_customer_id );
$sdk_client_token = $api->sdk_client_token();
$localized_script_data['axo'] = array(
'sdk_client_token' => $sdk_client_token,
);
@ -305,14 +325,6 @@ class AxoModule implements ModuleInterface {
return $localized_script_data;
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
/**
* Condition to evaluate if Credit Card gateway should be hidden.
*
@ -341,6 +353,7 @@ class AxoModule implements ModuleInterface {
return ! is_user_logged_in()
&& CartCheckoutDetector::has_classic_checkout()
&& $this->is_compatible_shipping_config()
&& $is_axo_enabled
&& $is_dcc_enabled
&& ! $this->is_excluded_endpoint();
@ -396,4 +409,32 @@ class AxoModule implements ModuleInterface {
// Exclude the Order Pay endpoint.
return is_wc_endpoint_url( 'order-pay' );
}
/**
* Condition to evaluate if the shipping configuration is compatible.
*
* @return bool
*/
private function is_compatible_shipping_config(): bool {
return ! wc_shipping_enabled() || ( wc_shipping_enabled() && ! wc_ship_to_billing_address_only() );
}
/**
* Outputs a meta tag to allow feature detection on certain pages.
*
* @param bool $axo_enabled Whether the gateway is enabled.
* @return void
*/
private function add_feature_detection_tag( bool $axo_enabled ) {
$show_tag = is_checkout() || is_cart() || is_shop();
if ( ! $show_tag ) {
return;
}
printf(
'<meta name="ppcp.axo" content="ppcp.axo.%s" />',
$axo_enabled ? 'enabled' : 'disabled'
);
}
}

View file

@ -168,7 +168,7 @@ class AxoGateway extends WC_Payment_Gateway {
? $this->ppcp_settings->get( 'axo_gateway_title' )
: $this->get_option( 'title', $this->method_title );
$this->description = __( 'Enter your email address to continue.', 'woocommerce-paypal-payments' );
$this->description = __( 'Enter your email address above to continue.', 'woocommerce-paypal-payments' );
$this->init_form_fields();
$this->init_settings();

View file

@ -64,19 +64,4 @@ class ApmApplies {
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
}
/**
* Returns whether the settings are compatible with AXO.
*
* @return bool
*/
public function for_settings(): bool {
if ( get_option( 'woocommerce_ship_to_destination' ) === 'billing_only' ) { // Force shipping to the customer billing address.
return false;
}
return true;
}
}

View file

@ -0,0 +1,179 @@
<?php
/**
* Settings notice generator.
* Generates the settings notices.
*
* @package WooCommerce\PayPalCommerce\Axo\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Helper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
/**
* Class SettingsNoticeGenerator
*/
class SettingsNoticeGenerator {
/**
* Generates the full HTML of the notification.
*
* @param string $message HTML of the inner message contents.
* @param bool $is_error Whether the provided message is an error. Affects the notice color.
*
* @return string The full HTML code of the notification, or an empty string.
*/
private function render_notice( string $message, bool $is_error = false ) : string {
if ( ! $message ) {
return '';
}
return sprintf(
'<div class="ppcp-notice %1$s"><p>%2$s</p></div>',
$is_error ? 'ppcp-notice-error' : '',
$message
);
}
/**
* Generates the checkout notice.
*
* @return string
*/
public function generate_checkout_notice(): string {
$checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
$block_checkout_docs_link = __(
'https://woocommerce.com/document/cart-checkout-blocks-status/#reverting-to-the-cart-and-checkout-shortcodes',
'woocommerce-paypal-payments'
);
$notice_content = '';
if ( CartCheckoutDetector::has_elementor_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the <code>Elementor Checkout widget</code>. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
} elseif ( CartCheckoutDetector::has_block_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the WooCommerce <code>Checkout</code> block. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
} elseif ( ! CartCheckoutDetector::has_classic_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store does not seem to be properly configured or uses an incompatible <code>third-party Checkout</code> solution. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
}
return $notice_content ? '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>' : '';
}
/**
* Generates the shipping notice.
*
* @return string
*/
public function generate_shipping_notice(): string {
$shipping_settings_link = admin_url( 'admin.php?page=wc-settings&tab=shipping&section=options' );
$notice_content = '';
if ( wc_shipping_enabled() && wc_ship_to_billing_address_only() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Shipping destination settings page. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Shipping destination</a> of your store is currently configured to <code>Force shipping to the customer billing address</code>. To enable Fastlane and accelerate payments, the shipping destination must be configured either to <code>Default to customer shipping address</code> or <code>Default to customer billing address</code> so buyers can set separate billing and shipping details.',
'woocommerce-paypal-payments'
),
esc_url( $shipping_settings_link )
);
}
return $notice_content ? '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>' : '';
}
/**
* Generates the incompatible plugins notice.
*
* @return string
*/
public function generate_incompatible_plugins_notice(): string {
$incompatible_plugins = array(
'Elementor' => did_action( 'elementor/loaded' ),
'CheckoutWC' => defined( 'CFW_NAME' ),
);
$active_plugins_list = array_filter( $incompatible_plugins );
if ( empty( $active_plugins_list ) ) {
return '';
}
$incompatible_plugin_items = array_map(
function ( $plugin ) {
return "<li>{$plugin}</li>";
},
array_keys( $active_plugins_list )
);
$plugins_settings_link = esc_url( admin_url( 'plugins.php' ) );
$notice_content = sprintf(
/* translators: %1$s: URL to the plugins settings page. %2$s: List of incompatible plugins. */
__(
'<span class="highlight">Note:</span> The accelerated guest buyer experience provided by Fastlane may not be fully compatible with some of the following <a href="%1$s">active plugins</a>: <ul class="ppcp-notice-list">%2$s</ul>',
'woocommerce-paypal-payments'
),
$plugins_settings_link,
implode( '', $incompatible_plugin_items )
);
return '<div class="ppcp-notice"><p>' . $notice_content . '</p></div>';
}
/**
* Generates a warning notice with instructions on conflicting plugin-internal settings.
*
* @param Settings $settings The plugin settings container, which is checked for conflicting
* values.
* @return string
*/
public function generate_settings_conflict_notice( Settings $settings ) : string {
$notice_content = '';
$is_dcc_enabled = false;
try {
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' );
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
} catch ( NotFoundException $ignored ) {
// Never happens.
}
if ( ! $is_dcc_enabled ) {
$notice_content = __(
'<span class="highlight">Warning:</span> To enable Fastlane and accelerate payments, the <strong>Advanced Card Processing</strong> payment method must also be enabled.',
'woocommerce-paypal-payments'
);
}
return $this->render_notice( $notice_content, true );
}
}

View file

@ -4,7 +4,7 @@
"description": "Blocks module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -13,7 +13,7 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'wcgateway.button.locations' => function ( ContainerInterface $container, array $locations ): array {
'wcgateway.button.locations' => function ( array $locations, ContainerInterface $container ): array {
return array_merge(
$locations,
array(
@ -22,13 +22,13 @@ return array(
)
);
},
'wcgateway.settings.pay-later.messaging-locations' => function ( ContainerInterface $container, array $locations ): array {
'wcgateway.settings.pay-later.messaging-locations' => function ( array $locations, ContainerInterface $container ): array {
unset( $locations['checkout-block-express'] );
unset( $locations['cart-block'] );
return $locations;
},
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
$insert_after = function( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
$index = array_search( $key, $keys, true );
@ -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',
@ -72,7 +72,7 @@ return array(
);
},
'button.pay-now-contexts' => function ( ContainerInterface $container, array $contexts ): array {
'button.pay-now-contexts' => function ( array $contexts, ContainerInterface $container ): array {
if ( ! $container->get( 'blocks.settings.final_review_enabled' ) ) {
$contexts[] = 'checkout-block';
$contexts[] = 'cart-block';
@ -81,7 +81,7 @@ return array(
return $contexts;
},
'button.handle-shipping-in-paypal' => function ( ContainerInterface $container ): bool {
'button.handle-shipping-in-paypal' => function ( bool $previous, ContainerInterface $container ): bool {
return ! $container->get( 'blocks.settings.final_review_enabled' );
},
);

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): BlocksModule {
return new BlocksModule();
};

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

@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\Blocks\Endpoint\GetPayPalOrderFromSession;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WC_Cart;
return array(
'blocks.url' => static function ( ContainerInterface $container ): string {
@ -27,6 +28,13 @@ return array(
);
},
'blocks.method' => static function ( ContainerInterface $container ): PayPalPaymentMethod {
/**
* Cart instance; might be null, esp. in customizer or in Block Editor.
*
* @var null|WC_Cart $cart
*/
$cart = WC()->cart;
return new PayPalPaymentMethod(
$container->get( 'blocks.url' ),
$container->get( 'ppcp.asset-version' ),
@ -43,7 +51,8 @@ return array(
$container->get( 'wcgateway.use-place-order-button' ),
$container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'wcgateway.place-order-button-description' ),
$container->get( 'wcgateway.all-funding-sources' )
$container->get( 'wcgateway.all-funding-sources' ),
$cart && $cart->needs_shipping()
);
},
'blocks.advanced-card-method' => static function( ContainerInterface $container ): AdvancedCardPaymentMethod {

View file

@ -52,7 +52,7 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType {
*
* @var Settings
*/
protected $settings;
protected $plugin_settings;
/**
* AdvancedCardPaymentMethod constructor.
@ -70,12 +70,12 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType {
$smart_button,
Settings $settings
) {
$this->name = CreditCardGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->name = CreditCardGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->plugin_settings = $settings;
}
/**
@ -118,8 +118,8 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType {
'scriptData' => $script_data,
'supports' => $this->gateway->supports,
'save_card_text' => esc_html__( 'Save your card', 'woocommerce-paypal-payments' ),
'is_vaulting_enabled' => $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ),
'card_icons' => $this->settings->has( 'card_icons' ) ? (array) $this->settings->get( 'card_icons' ) : array(),
'is_vaulting_enabled' => $this->plugin_settings->has( 'vault_enabled_dcc' ) && $this->plugin_settings->get( 'vault_enabled_dcc' ),
'card_icons' => $this->plugin_settings->has( 'card_icons' ) ? (array) $this->plugin_settings->get( 'card_icons' ) : array(),
);
}

View file

@ -12,29 +12,36 @@ namespace WooCommerce\PayPalCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
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\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;
/**
* Class BlocksModule
*/
class BlocksModule implements ModuleInterface {
class BlocksModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
if (
! class_exists( 'Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType' )
|| ! function_exists( 'woocommerce_store_api_register_payment_requirements' )
@ -54,7 +61,7 @@ class BlocksModule implements ModuleInterface {
}
);
return;
return true;
}
add_action(
@ -118,13 +125,6 @@ class BlocksModule implements ModuleInterface {
return $components;
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
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

@ -122,6 +122,13 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
*/
private $all_funding_sources;
/**
* Whether shipping details must be collected during checkout; i.e. paying for physical goods?
*
* @var bool
*/
private $need_shipping;
/**
* Assets constructor.
*
@ -139,6 +146,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
* @param string $place_order_button_text The text for the standard "Place order" button.
* @param string $place_order_button_description The text for additional "Place order" description.
* @param array $all_funding_sources All existing funding sources for PayPal buttons.
* @param bool $need_shipping Whether shipping details are required for the purchase.
*/
public function __construct(
string $module_url,
@ -154,7 +162,8 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
bool $use_place_order,
string $place_order_button_text,
string $place_order_button_description,
array $all_funding_sources
array $all_funding_sources,
bool $need_shipping
) {
$this->name = PayPalGateway::ID;
$this->module_url = $module_url;
@ -171,6 +180,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
$this->place_order_button_text = $place_order_button_text;
$this->place_order_button_description = $place_order_button_description;
$this->all_funding_sources = $all_funding_sources;
$this->need_shipping = $need_shipping;
}
/**
@ -254,6 +264,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
),
),
'scriptData' => $script_data,
'needShipping' => $this->need_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

@ -4,7 +4,7 @@
"description": "Button module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): ButtonModule {
return new ButtonModule();
};

View file

@ -1,11 +1,10 @@
#payment ul.payment_methods li img.ppcp-card-icon {
padding: 0 0 3px 3px;
max-height: 25px;
display: inline-block;
#payment ul.payment_methods [class*="payment_method_ppcp-"] label img {
max-height: 25px;
display: inline-block;
}
.payments-sdk-contingency-handler {
z-index: 1000 !important;
z-index: 1000 !important;
}
.ppcp-credit-card-gateway-form-field-disabled {

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;
@ -181,9 +197,17 @@ class CheckoutBootstap {
const isSeparateButtonGateway = [ PaymentMethods.CARD_BUTTON ].includes(
currentPaymentMethod
);
const isGooglePayMethod =
currentPaymentMethod === PaymentMethods.GOOGLEPAY;
const isApplePayMethod =
currentPaymentMethod === PaymentMethods.APPLEPAY;
const isSavedCard = isCard && isSavedCardSelected();
const isNotOurGateway =
! isPaypal && ! isCard && ! isSeparateButtonGateway;
! isPaypal &&
! isCard &&
! isSeparateButtonGateway &&
! isGooglePayMethod &&
! isApplePayMethod;
const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart;
const hasVaultedPaypal =
PayPalCommerceGateway.vaulted_paypal_email !== '';
@ -227,7 +251,20 @@ class CheckoutBootstap {
}
}
jQuery( document.body ).trigger( 'ppcp_checkout_rendered' );
/**
* 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,
} );
setVisible( '#ppc-button-ppcp-applepay', isApplePayMethod );
document.body.dispatchEvent( new Event( 'ppcp_checkout_rendered' ) );
}
shouldShowMessages() {

View file

@ -7,6 +7,7 @@ import { getPlanIdFromVariation } from '../Helper/Subscriptions';
import SimulateCart from '../Helper/SimulateCart';
import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils';
import merge from 'deepmerge';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
class SingleProductBootstap {
constructor( gateway, renderer, errorHandler ) {
@ -20,9 +21,13 @@ class SingleProductBootstap {
// Prevent simulate cart being called too many times in a burst.
this.simulateCartThrottled = throttle(
this.simulateCart,
this.simulateCart.bind( this ),
this.gateway.simulate_cart.throttling || 5000
);
this.debouncedHandleChange = debounce(
this.handleChange.bind( this ),
100
);
this.renderer.onButtonsInit(
this.gateway.button.wrapper,
@ -74,7 +79,7 @@ class SingleProductBootstap {
}
jQuery( document ).on( 'change', this.formSelector, () => {
this.handleChange();
this.debouncedHandleChange();
} );
this.mutationObserver.observe( form, {
childList: true,

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

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

View file

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

View file

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

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

@ -71,7 +71,10 @@ export const loadPaypalScript = ( config, onLoaded, onError = null ) => {
}
// Load PayPal script for special case with data-client-token
if ( config.data_client_id?.set_attribute ) {
if (
config.data_client_id?.set_attribute &&
config.vault_v3_enabled !== '1'
) {
dataClientIdAttributeHandler(
scriptOptions,
config.data_client_id,

View file

@ -1,34 +1,49 @@
const onApprove = ( context, errorHandler ) => {
return ( data, actions ) => {
const canCreateOrder =
! context.config.vaultingEnabled || data.paymentSource !== 'venmo';
const payload = {
nonce: context.config.ajax.approve_order.nonce,
order_id: data.orderID,
funding_source: window.ppcpFundingSource,
should_create_wc_order: canCreateOrder,
};
if ( canCreateOrder && data.payer ) {
payload.payer = data.payer;
}
return fetch( context.config.ajax.approve_order.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: context.config.ajax.approve_order.nonce,
order_id: data.orderID,
funding_source: window.ppcpFundingSource,
should_create_wc_order:
! context.config.vaultingEnabled ||
data.paymentSource !== 'venmo',
} ),
body: JSON.stringify( payload ),
} )
.then( ( res ) => {
return res.json();
} )
.then( ( data ) => {
if ( ! data.success ) {
.then( ( approveData ) => {
if ( ! approveData.success ) {
errorHandler.genericError();
return actions.restart().catch( ( err ) => {
errorHandler.genericError();
} );
}
const orderReceivedUrl = data.data?.order_received_url;
const orderReceivedUrl = approveData.data?.order_received_url;
location.href = orderReceivedUrl
/**
* Notice how this step initiates a redirect to a new page using a plain
* URL as new location. This process does not send any details about the
* approved order or billed customer.
* Also, due to the redirect starting _instantly_ there should be no other
* logic scheduled after calling `await onApprove()`;
*/
window.location.href = orderReceivedUrl
? orderReceivedUrl
: context.config.redirect;
} );

View file

@ -0,0 +1,52 @@
import PreviewButton from './PreviewButton';
/**
* Dummy preview button, to use in case an APM button cannot be rendered
*/
export default class DummyPreviewButton extends PreviewButton {
#innerEl;
constructor( args ) {
super( args );
this.selector = `${ args.selector }Dummy`;
this.label = args.label || 'Not Available';
}
createNewWrapper() {
const wrapper = super.createNewWrapper();
wrapper.classList.add( 'ppcp-button-apm', 'ppcp-button-dummy' );
return wrapper;
}
createButton( buttonConfig ) {
this.#innerEl?.remove();
this.#innerEl = document.createElement( 'div' );
this.#innerEl.innerHTML = `<div class="reason">${ this.label }</div>`;
this._applyStyles( this.ppcpConfig?.button?.style );
this.domWrapper.appendChild( this.#innerEl );
}
/**
* Applies the button shape (rect/pill) to the dummy button
*
* @param {{shape: string, height: number|null}} style
* @private
*/
_applyStyles( style ) {
this.domWrapper.classList.remove(
'ppcp-button-pill',
'ppcp-button-rect'
);
this.domWrapper.classList.add( `ppcp-button-${ style.shape }` );
if ( style.height ) {
this.domWrapper.style.height = `${ style.height }px`;
}
}
}

View file

@ -5,16 +5,21 @@ import merge from 'deepmerge';
*/
class PreviewButton {
/**
* @param {string} selector - CSS ID of the wrapper, including the `#`
* @param {Object} apiConfig - PayPal configuration object; retrieved via a
* widgetBuilder API method
* @param {string} selector - CSS ID of the wrapper, including the `#`
* @param {Object} apiConfig - PayPal configuration object; retrieved via a
* widgetBuilder API method
* @param {string} methodName - Name of the payment method, e.g. "Google Pay"
*/
constructor( { selector, apiConfig } ) {
constructor( { selector, apiConfig, methodName = '' } ) {
this.apiConfig = apiConfig;
this.defaultAttributes = {};
this.buttonConfig = {};
this.ppcpConfig = {};
this.isDynamic = true;
this.methodName = methodName;
this.methodSlug = this.methodName
.toLowerCase()
.replace( /[^a-z]+/g, '' );
// The selector is usually overwritten in constructor of derived class.
this.selector = selector;
@ -26,13 +31,16 @@ class PreviewButton {
/**
* Creates a new DOM node to contain the preview button.
*
* @return {jQuery} Always a single jQuery element with the new DOM node.
* @return {HTMLElement} Always a single jQuery element with the new DOM node.
*/
createNewWrapper() {
const wrapper = document.createElement( 'div' );
const previewId = this.selector.replace( '#', '' );
const previewClass = 'ppcp-button-apm';
const previewClass = `ppcp-preview-button ppcp-button-apm ppcp-button-${ this.methodSlug }`;
return jQuery( `<div id='${ previewId }' class='${ previewClass }'>` );
wrapper.setAttribute( 'id', previewId );
wrapper.setAttribute( 'class', previewClass );
return wrapper;
}
/**
@ -109,10 +117,12 @@ class PreviewButton {
console.error( 'Skip render, button is not configured yet' );
return;
}
this.domWrapper = this.createNewWrapper();
this.domWrapper.insertAfter( this.wrapper );
this._insertWrapper();
} else {
this.domWrapper.empty().show();
this._emptyWrapper();
this._showWrapper();
}
this.isVisible = true;
@ -151,16 +161,38 @@ class PreviewButton {
* Using a timeout here will make the button visible again at the end of the current
* event queue.
*/
setTimeout( () => this.domWrapper.show() );
setTimeout( () => this._showWrapper() );
}
remove() {
this.isVisible = false;
if ( this.domWrapper ) {
this.domWrapper.hide().empty();
this._hideWrapper();
this._emptyWrapper();
}
}
_showWrapper() {
this.domWrapper.style.display = '';
}
_hideWrapper() {
this.domWrapper.style.display = 'none';
}
_emptyWrapper() {
this.domWrapper.innerHTML = '';
}
_insertWrapper() {
const wrapperElement = document.querySelector( this.wrapper );
wrapperElement.parentNode.insertBefore(
this.domWrapper,
wrapperElement.nextSibling
);
}
}
export default PreviewButton;

View file

@ -1,11 +1,18 @@
import { loadCustomScript } from '@paypal/paypal-js';
import widgetBuilder from './WidgetBuilder';
import widgetBuilder from '../Renderer/WidgetBuilder';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
import DummyPreviewButton from './DummyPreviewButton';
/**
* 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.
@ -20,6 +27,13 @@ class PreviewButtonManager {
*/
#onInit;
/**
* Initialize the new PreviewButtonManager.
*
* @param {string} methodName - Name of the payment method, e.g. "Google Pay"
* @param {Object} buttonConfig
* @param {Object} defaultAttributes
*/
constructor( { methodName, buttonConfig, defaultAttributes } ) {
// Define the payment method name in the derived class.
this.methodName = methodName;
@ -32,6 +46,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 +78,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 +93,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'
@ -89,27 +110,15 @@ 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 ) {
const elButton = document.createElement( 'div' );
elButton.classList.add( 'ppcp-button-apm', 'ppcp-button-dummy' );
elButton.innerHTML = `<span>${
this.apiError ?? 'Not Available'
}</span>`;
document.querySelector( wrapperId ).appendChild( elButton );
const instDummy = {
setDynamic: () => instDummy,
setPpcpConfig: () => instDummy,
render: () => {},
remove: () => {},
};
return instDummy;
createDummyButtonInstance( wrapperId ) {
return new DummyPreviewButton( {
selector: wrapperId,
label: this.apiError,
methodName: this.methodName,
} );
}
registerEventListeners() {
@ -128,13 +137,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 +262,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 +291,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,10 +307,25 @@ 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 ] ) => {
const limitWrapper = ppcpConfig.button?.wrapper;
/**
* When the ppcpConfig object specifies a button wrapper, then ensure to limit preview
* changes to this individual wrapper. If no button wrapper is defined, the
* configuration is relevant for all buttons on the page.
*/
if ( limitWrapper && button.wrapper !== limitWrapper ) {
return;
}
this._configureButton( id, {
...ppcpConfig,
button: {
@ -302,20 +341,26 @@ 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
);
newInst = this.createButtonInstance( id );
} else {
newInst = this.createDummy( id );
newInst = this.createDummyButtonInstance( id );
}
newInst.setButtonConfig( this.buttonConfig );
this.buttons[ id ] = newInst;
}

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,865 @@
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;
/**
* Whether the one-time initialization of the payment gateway is complete.
*
* @type {boolean}
*/
#gatewayInitialized = 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;
}
/**
* 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
* 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 &&
this.isInsideClassicGateway
);
}
/**
* Whether the currently selected payment gateway is set to the payment method.
*
* 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.isInsideClassicGateway ) {
// This means, the button's visibility is managed by another script.
return true;
}
/*
* 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.
*/
const currentMethod = getCurrentPaymentMethod();
if ( this.isSeparateGateway ) {
return this.methodId === currentMethod;
}
// Button is rendered inside the Smart Buttons block.
return PaymentMethods.PAYPAL === currentMethod;
}
/**
* 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 );
}
/**
* Open or close a log-group
*
* @param {?string} [label=null] Group label.
*/
logGroup( label = null ) {
this.#logger.group( label );
}
/**
* 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 ( this.isInsideClassicGateway ) {
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.isCurrentGateway && this.isVisible ) {
if ( ! this.isButtonAttached ) {
this.log( 'refresh.addButton' );
this.addButton();
}
}
}
/**
* 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.#gatewayInitialized ||
! this.isSeparateGateway ||
! this.isEligible
) {
return;
}
const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
const wrapperSelector = `#${ this.wrappers.Default }`;
document
.querySelectorAll( styleSelector )
.forEach( ( el ) => el.remove() );
document
.querySelectorAll( wrapperSelector )
.forEach( ( el ) => el.remove() );
this.log( 'Show gateway' );
this.#gatewayInitialized = true;
// 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

@ -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;
@ -50,6 +51,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WC_Shipping_Method;
use WC_Cart;
/**
* Class SmartButton
@ -1084,6 +1087,22 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
return apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $contingency );
}
/**
* Whether the current cart contains a product that requires physical shipping.
*
* @return bool True, if any cart item requires shipping.
*/
private function need_shipping() : bool {
/**
* Cart instance; might be null, esp. in customizer or in Block Editor.
*
* @var null|WC_Cart $cart
*/
$cart = WC()->cart;
return $cart && $cart->needs_shipping();
}
/**
* The configuration for the smart buttons.
*
@ -1292,9 +1311,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' => $this->need_shipping(),
'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ),
);
@ -2132,4 +2153,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

@ -14,8 +14,6 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
@ -23,29 +21,36 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
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;
/**
* Class ButtonModule
*/
class ButtonModule implements ModuleInterface {
class ButtonModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
add_action(
'wp',
@ -91,6 +96,8 @@ class ButtonModule implements ModuleInterface {
);
$this->register_ajax_endpoints( $c );
return true;
}
/**
@ -211,12 +218,4 @@ class ButtonModule implements ModuleInterface {
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
}

View file

@ -85,7 +85,8 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
$this->add_products( $products );
$this->cart->calculate_totals();
$total = (float) $this->cart->get_total( 'numeric' );
$total = (float) $this->cart->get_total( 'numeric' );
$shipping_fee = (float) $this->cart->get_shipping_total();
$this->restore_real_cart();
@ -113,7 +114,7 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
wp_send_json_success(
array(
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'shipping_fee' => $shipping_fee,
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
'funding' => array(

View file

@ -9,13 +9,17 @@ 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;
use WC_Product;
use WC_Subscription;
use WC_Subscriptions_Product;
use WC_Tax;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
@ -83,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;
}
@ -106,6 +117,7 @@ class WooCommerceOrderCreator {
* @param Payer|null $payer The payer.
* @param Shipping|null $shipping The shipping.
* @return void
* @psalm-suppress InvalidScalarArgument
*/
protected function configure_line_items( WC_Order $wc_order, WC_Cart $wc_cart, ?Payer $payer, ?Shipping $shipping ): void {
$cart_contents = $wc_cart->get_cart();
@ -130,24 +142,27 @@ class WooCommerceOrderCreator {
return;
}
$total = $product->get_price() * $quantity;
$subtotal = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
$subtotal = apply_filters( 'woocommerce_paypal_payments_shipping_callback_cart_line_item_total', $subtotal, $cart_item );
$item->set_name( $product->get_name() );
$item->set_subtotal( $total );
$item->set_total( $total );
$item->set_subtotal( $subtotal );
$item->set_total( $subtotal );
$this->configure_taxes( $product, $item, $subtotal );
$product_id = $product->get_id();
if ( $this->is_subscription( $product_id ) ) {
$subscription = $this->create_subscription( $wc_order, $product_id );
$sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
$subscription_total = $total + $sign_up_fee;
$subscription_total = (float) $subtotal + (float) $sign_up_fee;
$item->set_subtotal( $subscription_total );
$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() );
@ -172,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;
@ -212,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 );
}
@ -282,6 +303,23 @@ class WooCommerceOrderCreator {
}
}
/**
* Configures the taxes.
*
* @param WC_Product $product The Product.
* @param WC_Order_Item_Product $item The line item.
* @param float|string $subtotal The subtotal.
* @return void
* @psalm-suppress InvalidScalarArgument
*/
protected function configure_taxes( WC_Product $product, WC_Order_Item_Product $item, $subtotal ): void {
$tax_rates = WC_Tax::get_rates( $product->get_tax_class() );
$taxes = WC_Tax::calc_tax( $subtotal, $tax_rates, true );
$item->set_tax_class( $product->get_tax_class() );
$item->set_total_tax( (float) array_sum( $taxes ) );
}
/**
* Checks if the product with given ID is WC subscription.
*

View file

@ -4,7 +4,7 @@
"description": "Advanced Checkout Card Fields module",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\CardFields;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): CardFieldsModule {
return new CardFieldsModule();
};

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"
}
}

Some files were not shown because too many files have changed in this diff Show more