mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-04 08:47:23 +08:00
Merge branch 'trunk' of github.com:woocommerce/woocommerce-paypal-payments into PCP-3202-retrieve-button-styling-properties-from-woo-commerce-checkout-block-ver-2
This commit is contained in:
commit
79cdf84618
544 changed files with 87210 additions and 6892 deletions
14
modules/ppcp-admin-notices/.babelrc
Normal file
14
modules/ppcp-admin-notices/.babelrc
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": "3.25.0"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/preset-react"
|
||||
]
|
||||
]
|
||||
}
|
|
@ -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": {
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
31
modules/ppcp-admin-notices/package.json
Normal file
31
modules/ppcp-admin-notices/package.json
Normal 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"
|
||||
}
|
||||
}
|
10
modules/ppcp-admin-notices/resources/css/styles.scss
Normal file
10
modules/ppcp-admin-notices/resources/css/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
137
modules/ppcp-admin-notices/resources/js/DismissibleMessage.js
Normal file
137
modules/ppcp-admin-notices/resources/js/DismissibleMessage.js
Normal 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' ) );
|
||||
}
|
||||
}
|
27
modules/ppcp-admin-notices/resources/js/boot-admin.js
Normal file
27
modules/ppcp-admin-notices/resources/js/boot-admin.js
Normal 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 );
|
|
@ -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' )
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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'] ?? '' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
125
modules/ppcp-admin-notices/src/Entity/PersistentMessage.php
Normal file
125
modules/ppcp-admin-notices/src/Entity/PersistentMessage.php
Normal 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 ) . '%'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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() ),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
38
modules/ppcp-admin-notices/webpack.config.js
Normal file
38
modules/ppcp-admin-notices/webpack.config.js
Normal 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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
2158
modules/ppcp-admin-notices/yarn.lock
Normal file
2158
modules/ppcp-admin-notices/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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": {
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,31 +14,37 @@ 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 ) {
|
||||
|
@ -96,13 +102,7 @@ class ApiModule implements ModuleInterface {
|
|||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key for the module.
|
||||
*
|
||||
* @return string|void
|
||||
*/
|
||||
public function getKey() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 UserIdToken {
|
|||
|
||||
use RequestTrait;
|
||||
|
||||
const CACHE_KEY = 'id-token-key';
|
||||
|
||||
/**
|
||||
* The host.
|
||||
*
|
||||
|
@ -41,21 +44,31 @@ class UserIdToken {
|
|||
*/
|
||||
private $client_credentials;
|
||||
|
||||
/**
|
||||
* The cache.
|
||||
*
|
||||
* @var Cache
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* UserIdToken constructor.
|
||||
*
|
||||
* @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,
|
||||
LoggerInterface $logger,
|
||||
ClientCredentials $client_credentials
|
||||
ClientCredentials $client_credentials,
|
||||
Cache $cache
|
||||
) {
|
||||
$this->host = $host;
|
||||
$this->logger = $logger;
|
||||
$this->client_credentials = $client_credentials;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,6 +82,15 @@ class UserIdToken {
|
|||
* @throws RuntimeException If something unexpected happens.
|
||||
*/
|
||||
public function id_token( string $target_customer_id = '' ): string {
|
||||
$session_customer_id = '';
|
||||
if ( ! is_null( WC()->session ) && method_exists( WC()->session, 'get_customer_id' ) ) {
|
||||
$session_customer_id = WC()->session->get_customer_id();
|
||||
}
|
||||
|
||||
if ( $session_customer_id && $this->cache->has( self::CACHE_KEY . (string) $session_customer_id ) ) {
|
||||
return $this->cache->get( self::CACHE_KEY . (string) $session_customer_id );
|
||||
}
|
||||
|
||||
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=id_token';
|
||||
if ( $target_customer_id ) {
|
||||
$url = add_query_arg(
|
||||
|
@ -98,6 +120,12 @@ class UserIdToken {
|
|||
throw new PayPalApiException( $json, $status_code );
|
||||
}
|
||||
|
||||
return $json->id_token;
|
||||
$id_token = $json->id_token;
|
||||
|
||||
if ( $session_customer_id ) {
|
||||
$this->cache->set( self::CACHE_KEY . (string) $session_customer_id, $id_token, 5 );
|
||||
}
|
||||
|
||||
return $id_token;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ class BillingSubscriptions {
|
|||
*/
|
||||
public function suspend( string $id ):void {
|
||||
$data = array(
|
||||
'reason' => 'Suspended by customer',
|
||||
'reason' => sprintf( 'Suspended by %s.', is_admin() ? 'merchant' : 'customer' ),
|
||||
);
|
||||
|
||||
$bearer = $this->bearer->bearer();
|
||||
|
@ -107,7 +107,7 @@ class BillingSubscriptions {
|
|||
*/
|
||||
public function activate( string $id ): void {
|
||||
$data = array(
|
||||
'reason' => 'Reactivated by customer',
|
||||
'reason' => sprintf( 'Reactivated by %s.', is_admin() ? 'merchant' : 'customer' ),
|
||||
);
|
||||
|
||||
$bearer = $this->bearer->bearer();
|
||||
|
@ -148,7 +148,7 @@ class BillingSubscriptions {
|
|||
*/
|
||||
public function cancel( string $id ): void {
|
||||
$data = array(
|
||||
'reason' => 'Cancelled by customer',
|
||||
'reason' => sprintf( 'Cancelled by %s.', is_admin() ? 'merchant' : 'customer' ),
|
||||
);
|
||||
|
||||
$bearer = $this->bearer->bearer();
|
||||
|
|
206
modules/ppcp-api-client/src/Endpoint/Orders.php
Normal file
206
modules/ppcp-api-client/src/Endpoint/Orders.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AmountBreakdown;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
|
@ -41,20 +42,24 @@ class AmountFactory {
|
|||
private $money_factory;
|
||||
|
||||
/**
|
||||
* 3-letter currency code of the shop.
|
||||
* The getter of the 3-letter currency code of the shop.
|
||||
*
|
||||
* @var string
|
||||
* @var CurrencyGetter
|
||||
*/
|
||||
private $currency;
|
||||
private CurrencyGetter $currency;
|
||||
|
||||
/**
|
||||
* AmountFactory constructor.
|
||||
*
|
||||
* @param ItemFactory $item_factory The Item factory.
|
||||
* @param MoneyFactory $money_factory The Money factory.
|
||||
* @param string $currency 3-letter currency code of the shop.
|
||||
* @param ItemFactory $item_factory The Item factory.
|
||||
* @param MoneyFactory $money_factory The Money factory.
|
||||
* @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop.
|
||||
*/
|
||||
public function __construct( ItemFactory $item_factory, MoneyFactory $money_factory, string $currency ) {
|
||||
public function __construct(
|
||||
ItemFactory $item_factory,
|
||||
MoneyFactory $money_factory,
|
||||
CurrencyGetter $currency
|
||||
) {
|
||||
$this->item_factory = $item_factory;
|
||||
$this->money_factory = $money_factory;
|
||||
$this->currency = $currency;
|
||||
|
@ -68,25 +73,25 @@ class AmountFactory {
|
|||
* @return Amount
|
||||
*/
|
||||
public function from_wc_cart( \WC_Cart $cart ): Amount {
|
||||
$total = new Money( (float) $cart->get_total( 'numeric' ), $this->currency );
|
||||
$total = new Money( (float) $cart->get_total( 'numeric' ), $this->currency->get() );
|
||||
|
||||
$item_total = (float) $cart->get_subtotal() + (float) $cart->get_fee_total();
|
||||
$item_total = new Money( $item_total, $this->currency );
|
||||
$item_total = new Money( $item_total, $this->currency->get() );
|
||||
$shipping = new Money(
|
||||
(float) $cart->get_shipping_total(),
|
||||
$this->currency
|
||||
$this->currency->get()
|
||||
);
|
||||
|
||||
$taxes = new Money(
|
||||
(float) $cart->get_total_tax(),
|
||||
$this->currency
|
||||
$this->currency->get()
|
||||
);
|
||||
|
||||
$discount = null;
|
||||
if ( $cart->get_discount_total() ) {
|
||||
$discount = new Money(
|
||||
(float) $cart->get_discount_total(),
|
||||
$this->currency
|
||||
$this->currency->get()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
|||
use stdClass;
|
||||
use WC_Product;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\BillingCycle;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
|
||||
/**
|
||||
* Class BillingCycleFactory
|
||||
|
@ -21,16 +22,16 @@ class BillingCycleFactory {
|
|||
/**
|
||||
* The currency.
|
||||
*
|
||||
* @var string
|
||||
* @var CurrencyGetter
|
||||
*/
|
||||
private $currency;
|
||||
private CurrencyGetter $currency;
|
||||
|
||||
/**
|
||||
* BillingCycleFactory constructor.
|
||||
*
|
||||
* @param string $currency The currency.
|
||||
* @param CurrencyGetter $currency The currency.
|
||||
*/
|
||||
public function __construct( string $currency ) {
|
||||
public function __construct( CurrencyGetter $currency ) {
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
|
@ -51,7 +52,7 @@ class BillingCycleFactory {
|
|||
array(
|
||||
'fixed_price' => array(
|
||||
'value' => $product->get_meta( '_subscription_price' ),
|
||||
'currency_code' => $this->currency,
|
||||
'currency_code' => $this->currency->get(),
|
||||
),
|
||||
),
|
||||
(int) $product->get_meta( '_subscription_length' )
|
||||
|
|
|
@ -13,6 +13,7 @@ use WC_Product;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\ItemTrait;
|
||||
|
||||
/**
|
||||
|
@ -23,18 +24,18 @@ class ItemFactory {
|
|||
use ItemTrait;
|
||||
|
||||
/**
|
||||
* 3-letter currency code of the shop.
|
||||
* The getter of the 3-letter currency code of the shop.
|
||||
*
|
||||
* @var string
|
||||
* @var CurrencyGetter
|
||||
*/
|
||||
private $currency;
|
||||
private CurrencyGetter $currency;
|
||||
|
||||
/**
|
||||
* ItemFactory constructor.
|
||||
*
|
||||
* @param string $currency 3-letter currency code of the shop.
|
||||
* @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop.
|
||||
*/
|
||||
public function __construct( string $currency ) {
|
||||
public function __construct( CurrencyGetter $currency ) {
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
|
@ -62,7 +63,7 @@ class ItemFactory {
|
|||
$price = (float) $item['line_subtotal'] / (float) $item['quantity'];
|
||||
return new Item(
|
||||
$this->prepare_item_string( $product->get_name() ),
|
||||
new Money( $price, $this->currency ),
|
||||
new Money( $price, $this->currency->get() ),
|
||||
$quantity,
|
||||
$this->prepare_item_string( $product->get_description() ),
|
||||
null,
|
||||
|
@ -84,7 +85,7 @@ class ItemFactory {
|
|||
function ( \stdClass $fee ): Item {
|
||||
return new Item(
|
||||
$fee->name,
|
||||
new Money( (float) $fee->amount, $this->currency ),
|
||||
new Money( (float) $fee->amount, $this->currency->get() ),
|
||||
1,
|
||||
'',
|
||||
null
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
|||
use stdClass;
|
||||
use WC_Product;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentPreferences;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
|
||||
/**
|
||||
* Class PaymentPreferencesFactory
|
||||
|
@ -21,16 +22,16 @@ class PaymentPreferencesFactory {
|
|||
/**
|
||||
* The currency.
|
||||
*
|
||||
* @var string
|
||||
* @var CurrencyGetter
|
||||
*/
|
||||
private $currency;
|
||||
|
||||
/**
|
||||
* PaymentPreferencesFactory constructor.
|
||||
*
|
||||
* @param string $currency The currency.
|
||||
* @param CurrencyGetter $currency The currency.
|
||||
*/
|
||||
public function __construct( string $currency ) {
|
||||
public function __construct( CurrencyGetter $currency ) {
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
|
@ -44,7 +45,7 @@ class PaymentPreferencesFactory {
|
|||
return new PaymentPreferences(
|
||||
array(
|
||||
'value' => $product->get_meta( '_subscription_sign_up_fee' ) ?: '0',
|
||||
'currency_code' => $this->currency,
|
||||
'currency_code' => $this->currency->get(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
40
modules/ppcp-api-client/src/Helper/CurrencyGetter.php
Normal file
40
modules/ppcp-api-client/src/Helper/CurrencyGetter.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
/**
|
||||
* The wrapper for retrieving shop currency as late as possible,
|
||||
* to avoid early caching in services, e.g. before multi-currency filters were added.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
|
||||
|
||||
/**
|
||||
* Class CurrencyGetter
|
||||
*/
|
||||
class CurrencyGetter {
|
||||
/**
|
||||
* Returns the WC currency.
|
||||
*/
|
||||
public function get(): string {
|
||||
$currency = get_woocommerce_currency();
|
||||
if ( $currency ) {
|
||||
return $currency;
|
||||
}
|
||||
|
||||
$currency = get_option( 'woocommerce_currency' );
|
||||
if ( ! $currency ) {
|
||||
return 'NO_CURRENCY'; // Unlikely to happen.
|
||||
}
|
||||
|
||||
return $currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the WC currency.
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->get();
|
||||
}
|
||||
}
|
|
@ -30,11 +30,11 @@ class DccApplies {
|
|||
private $country_card_matrix;
|
||||
|
||||
/**
|
||||
* 3-letter currency code of the shop.
|
||||
* The getter of the 3-letter currency code of the shop.
|
||||
*
|
||||
* @var string
|
||||
* @var CurrencyGetter
|
||||
*/
|
||||
private $currency;
|
||||
private CurrencyGetter $currency;
|
||||
|
||||
/**
|
||||
* 2-letter country code of the shop.
|
||||
|
@ -46,16 +46,16 @@ class DccApplies {
|
|||
/**
|
||||
* DccApplies constructor.
|
||||
*
|
||||
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
|
||||
* @param array $country_card_matrix Which countries support which credit cards. Empty credit card arrays mean no restriction on
|
||||
* currency.
|
||||
* @param string $currency 3-letter currency code of the shop.
|
||||
* @param string $country 2-letter country code of the shop.
|
||||
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for DCC.
|
||||
* @param array $country_card_matrix Which countries support which credit cards. Empty credit card arrays mean no restriction on
|
||||
* currency.
|
||||
* @param CurrencyGetter $currency The getter of the 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 $country_card_matrix,
|
||||
string $currency,
|
||||
CurrencyGetter $currency,
|
||||
string $country
|
||||
) {
|
||||
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
|
||||
|
@ -73,7 +73,7 @@ class DccApplies {
|
|||
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
$applies = in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
|
||||
$applies = in_array( $this->currency->get(), $this->allowed_country_currency_matrix[ $this->country ], true );
|
||||
return $applies;
|
||||
}
|
||||
|
||||
|
@ -135,6 +135,6 @@ class DccApplies {
|
|||
* restrictions, which currencies are supported by a card.
|
||||
*/
|
||||
$supported_currencies = $this->country_card_matrix[ $this->country ][ $card ];
|
||||
return empty( $supported_currencies ) || in_array( $this->currency, $supported_currencies, true );
|
||||
return empty( $supported_currencies ) || in_array( $this->currency->get(), $supported_currencies, true );
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
83
modules/ppcp-applepay/assets/images/applepay.svg
Normal file
83
modules/ppcp-applepay/assets/images/applepay.svg
Normal 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 |
|
@ -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": {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,56 @@ 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() );
|
||||
|
||||
const paymentMethodAppleLi = document.querySelector('.wc_payment_method.payment_method_ppcp-applepay' );
|
||||
if (paymentMethodAppleLi.style.display === 'none' || paymentMethodAppleLi.style.display === '') {
|
||||
paymentMethodAppleLi.style.display = 'block';
|
||||
}
|
||||
|
||||
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 +437,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 +457,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 +472,7 @@ class ApplepayButton {
|
|||
session.onshippingcontactselected =
|
||||
this.onShippingContactSelected( session );
|
||||
}
|
||||
|
||||
session.onvalidatemerchant = this.onValidateMerchant( session );
|
||||
session.onpaymentauthorized = this.onPaymentAuthorized( session );
|
||||
return session;
|
||||
|
@ -199,32 +480,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 +523,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 +559,7 @@ class ApplepayButton {
|
|||
PayPalCommerceGateway.ajax.validate_checkout.nonce
|
||||
)
|
||||
: null;
|
||||
|
||||
if ( formValidator ) {
|
||||
try {
|
||||
const errors = await formValidator.validate(
|
||||
|
@ -296,13 +587,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 +601,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 +614,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 +648,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 +716,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 +745,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 +757,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 +796,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 +813,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 +835,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 +864,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 +872,7 @@ class ApplepayButton {
|
|||
const data = this.getShippingContactData( event );
|
||||
|
||||
jQuery.ajax( {
|
||||
url: ajax_url,
|
||||
url: ajaxUrl,
|
||||
method: 'POST',
|
||||
data,
|
||||
success: (
|
||||
|
@ -603,15 +902,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 +918,12 @@ class ApplepayButton {
|
|||
need_shipping: this.shouldRequireShippingInButton(),
|
||||
'woocommerce-process-checkout-nonce': this.nonce,
|
||||
};
|
||||
case 'cart':
|
||||
case 'checkout':
|
||||
case 'cart-block':
|
||||
case 'checkout-block':
|
||||
case 'mini-cart':
|
||||
|
||||
case CONTEXT.Cart:
|
||||
case CONTEXT.Checkout:
|
||||
case CONTEXT.BlockCart:
|
||||
case CONTEXT.BlockCheckout:
|
||||
case CONTEXT.MiniCart:
|
||||
return {
|
||||
action: 'ppcp_update_shipping_contact',
|
||||
simplified_contact: event.shippingContact,
|
||||
|
@ -635,12 +935,12 @@ class ApplepayButton {
|
|||
}
|
||||
|
||||
getShippingMethodData( event ) {
|
||||
const product_id = this.buttonConfig.product.id;
|
||||
const productId = this.buttonConfig.product.id;
|
||||
|
||||
this.refreshContextData();
|
||||
|
||||
switch ( this.context ) {
|
||||
case 'product':
|
||||
case CONTEXT.Product:
|
||||
return {
|
||||
action: 'ppcp_update_shipping_method',
|
||||
shipping_method: event.shippingMethod,
|
||||
|
@ -650,17 +950,18 @@ class ApplepayButton {
|
|||
? this.updatedContactInfo
|
||||
: this.initialPaymentRequest?.shippingContact ??
|
||||
this.initialPaymentRequest?.billingContact,
|
||||
product_id,
|
||||
product_id: productId,
|
||||
products: JSON.stringify( this.products ),
|
||||
caller_page: 'productDetail',
|
||||
product_quantity: this.productQuantity,
|
||||
'woocommerce-process-checkout-nonce': this.nonce,
|
||||
};
|
||||
case 'cart':
|
||||
case 'checkout':
|
||||
case 'cart-block':
|
||||
case 'checkout-block':
|
||||
case 'mini-cart':
|
||||
|
||||
case CONTEXT.Cart:
|
||||
case CONTEXT.Checkout:
|
||||
case CONTEXT.BlockCart:
|
||||
case CONTEXT.BlockCheckout:
|
||||
case CONTEXT.MiniCart:
|
||||
return {
|
||||
action: 'ppcp_update_shipping_method',
|
||||
shipping_method: event.shippingMethod,
|
||||
|
@ -681,9 +982,6 @@ class ApplepayButton {
|
|||
return async ( event ) => {
|
||||
this.log( 'onpaymentauthorized call' );
|
||||
|
||||
function form() {
|
||||
return document.querySelector( 'form.cart' );
|
||||
}
|
||||
const processInWooAndCapture = async ( data ) => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
try {
|
||||
|
@ -698,7 +996,7 @@ class ApplepayButton {
|
|||
( this.initialPaymentRequest.shippingMethods ||
|
||||
[] )[ 0 ];
|
||||
|
||||
const request_data = {
|
||||
const requestData = {
|
||||
action: 'ppcp_create_order',
|
||||
caller_page: this.context,
|
||||
product_id: this.buttonConfig.product.id ?? null,
|
||||
|
@ -723,7 +1021,7 @@ class ApplepayButton {
|
|||
jQuery.ajax( {
|
||||
url: this.buttonConfig.ajax_url,
|
||||
method: 'POST',
|
||||
data: request_data,
|
||||
data: requestData,
|
||||
complete: ( jqXHR, textStatus ) => {
|
||||
this.log( 'onpaymentauthorized complete' );
|
||||
},
|
||||
|
@ -785,7 +1083,8 @@ class ApplepayButton {
|
|||
if (
|
||||
this.shouldCompletePaymentWithContextHandler()
|
||||
) {
|
||||
// No shipping, expect immediate capture, ex: PayNow, Checkout with form data.
|
||||
// No shipping, expect immediate capture, ex: PayNow, Checkout with
|
||||
// form data.
|
||||
|
||||
let approveFailed = false;
|
||||
await this.contextHandler.approveOrder(
|
||||
|
@ -960,4 +1259,4 @@ class ApplepayButton {
|
|||
}
|
||||
}
|
||||
|
||||
export default ApplepayButton;
|
||||
export default ApplePayButton;
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
|
||||
import ApplepayButton from './ApplepayButton';
|
||||
import ApplePayButton from './ApplepayButton';
|
||||
|
||||
class ApplepayManager {
|
||||
constructor( buttonConfig, ppcpConfig ) {
|
||||
class ApplePayManager {
|
||||
constructor( namespace, buttonConfig, ppcpConfig ) {
|
||||
this.namespace = namespace;
|
||||
this.buttonConfig = buttonConfig;
|
||||
this.ppcpConfig = ppcpConfig;
|
||||
this.ApplePayConfig = null;
|
||||
this.buttons = [];
|
||||
|
||||
buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => {
|
||||
const button = new ApplepayButton(
|
||||
const button = new ApplePayButton(
|
||||
bootstrap.context,
|
||||
bootstrap.handler,
|
||||
buttonConfig,
|
||||
|
@ -40,13 +41,15 @@ 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();
|
||||
this.ApplePayConfig = await window[ this.namespace ]
|
||||
.Applepay()
|
||||
.config();
|
||||
|
||||
return this.ApplePayConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplepayManager;
|
||||
export default ApplePayManager;
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
import ApplepayButton from './ApplepayButton';
|
||||
import ApplePayButton from './ApplepayButton';
|
||||
|
||||
class ApplepayManagerBlockEditor {
|
||||
constructor( buttonConfig, ppcpConfig ) {
|
||||
class ApplePayManagerBlockEditor {
|
||||
constructor( namespace, buttonConfig, ppcpConfig ) {
|
||||
this.namespace = namespace;
|
||||
this.buttonConfig = buttonConfig;
|
||||
this.ppcpConfig = ppcpConfig;
|
||||
this.applePayConfig = null;
|
||||
|
||||
/*
|
||||
* On the front-end, the init method is called when a new button context was detected
|
||||
* via `buttonModuleWatcher`. In the block editor, we do not need to wait for the
|
||||
* context, but can initialize the button in the next event loop.
|
||||
*/
|
||||
setTimeout( () => this.init() );
|
||||
}
|
||||
|
||||
init() {
|
||||
( async () => {
|
||||
await this.config();
|
||||
} )();
|
||||
}
|
||||
|
||||
async config() {
|
||||
async init() {
|
||||
try {
|
||||
this.applePayConfig = await paypal.Applepay().config();
|
||||
this.applePayConfig = await window[ this.namespace ]
|
||||
.Applepay()
|
||||
.config();
|
||||
|
||||
const button = new ApplepayButton(
|
||||
const button = new ApplePayButton(
|
||||
this.ppcpConfig.context,
|
||||
null,
|
||||
this.buttonConfig,
|
||||
|
@ -31,4 +34,4 @@ class ApplepayManagerBlockEditor {
|
|||
}
|
||||
}
|
||||
|
||||
export default ApplepayManagerBlockEditor;
|
||||
export default ApplePayManagerBlockEditor;
|
||||
|
|
|
@ -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.' );
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class SingleProductHandler extends BaseHandler {
|
|||
countryCode: data.country_code,
|
||||
currencyCode: data.currency_code,
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: data.total_str,
|
||||
totalPrice: data.total,
|
||||
} );
|
||||
}, products );
|
||||
} );
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
} );
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { registerExpressPaymentMethod } from '@woocommerce/blocks-registry';
|
||||
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
|
||||
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
|
||||
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;
|
||||
|
||||
const buttonData = wc.wcSettings.getSetting( 'ppcp-applepay_data' );
|
||||
const buttonConfig = buttonData.scriptData;
|
||||
const namespace = 'ppcpBlocksPaypalApplepay';
|
||||
|
||||
if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
|
||||
window.PayPalCommerceGateway = ppcpConfig;
|
||||
|
@ -24,9 +25,9 @@ const ApplePayComponent = ( props ) => {
|
|||
|
||||
const bootstrap = function () {
|
||||
const ManagerClass = props.isEditing
|
||||
? ApplepayManagerBlockEditor
|
||||
: ApplepayManager;
|
||||
const manager = new ManagerClass( buttonConfig, ppcpConfig );
|
||||
? ApplePayManagerBlockEditor
|
||||
: ApplePayManager;
|
||||
const manager = new ManagerClass( namespace, buttonConfig, ppcpConfig );
|
||||
manager.init();
|
||||
};
|
||||
|
||||
|
@ -39,9 +40,13 @@ const ApplePayComponent = ( props ) => {
|
|||
ppcpConfig.url_params.components += ',applepay';
|
||||
|
||||
// Load PayPal
|
||||
loadPaypalScript( ppcpConfig, () => {
|
||||
setPaypalLoaded( true );
|
||||
} );
|
||||
loadPayPalScript( namespace, ppcpConfig )
|
||||
.then( () => {
|
||||
setPaypalLoaded( true );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
console.error( 'Failed to load PayPal script: ', error );
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
useEffect( () => {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { loadCustomScript } from '@paypal/paypal-js';
|
||||
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
|
||||
import ApplepayManager from './ApplepayManager';
|
||||
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
|
||||
import ApplePayManager from './ApplepayManager';
|
||||
import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper';
|
||||
|
||||
( function ( { buttonConfig, ppcpConfig, jQuery } ) {
|
||||
const namespace = 'ppcpPaypalApplepay';
|
||||
let manager;
|
||||
|
||||
const bootstrap = function () {
|
||||
manager = new ApplepayManager( buttonConfig, ppcpConfig );
|
||||
manager = new ApplePayManager( namespace, buttonConfig, ppcpConfig );
|
||||
manager.init();
|
||||
};
|
||||
|
||||
|
@ -50,10 +51,14 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
|
|||
} );
|
||||
|
||||
// Load PayPal
|
||||
loadPaypalScript( ppcpConfig, () => {
|
||||
paypalLoaded = true;
|
||||
tryToBoot();
|
||||
} );
|
||||
loadPayPalScript( namespace, ppcpConfig )
|
||||
.then( () => {
|
||||
paypalLoaded = true;
|
||||
tryToBoot();
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
console.error( 'Failed to load PayPal script: ', error );
|
||||
} );
|
||||
} );
|
||||
} )( {
|
||||
buttonConfig: window.wc_ppcp_applepay,
|
||||
|
|
|
@ -34,7 +34,7 @@ return array(
|
|||
return new ApmApplies(
|
||||
$container->get( 'applepay.supported-countries' ),
|
||||
$container->get( 'applepay.supported-currencies' ),
|
||||
$container->get( 'api.shop.currency' ),
|
||||
$container->get( 'api.shop.currency.getter' ),
|
||||
$container->get( 'api.shop.country' )
|
||||
);
|
||||
},
|
||||
|
@ -299,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' )
|
||||
);
|
||||
},
|
||||
|
||||
);
|
||||
|
|
242
modules/ppcp-applepay/src/ApplePayGateway.php
Normal file
242
modules/ppcp-applepay/src/ApplePayGateway.php
Normal 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 );
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -113,7 +113,7 @@ class BlocksPaymentMethod extends AbstractPaymentMethodType {
|
|||
'id' => $this->name,
|
||||
'title' => $paypal_data['title'], // TODO : see if we should use another.
|
||||
'description' => $paypal_data['description'], // TODO : see if we should use another.
|
||||
'enabled' => $paypal_data['enabled'], // This button is enabled when PayPal buttons are.
|
||||
'enabled' => $paypal_data['smartButtonsEnabled'], // This button is enabled when PayPal buttons are.
|
||||
'scriptData' => $script_data,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Applepay\Helper;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
|
||||
/**
|
||||
* Class ApmApplies
|
||||
*/
|
||||
|
@ -30,11 +32,11 @@ class ApmApplies {
|
|||
private $allowed_currencies;
|
||||
|
||||
/**
|
||||
* 3-letter currency code of the shop.
|
||||
* The getter of the 3-letter currency code of the shop.
|
||||
*
|
||||
* @var string
|
||||
* @var CurrencyGetter
|
||||
*/
|
||||
private $currency;
|
||||
private CurrencyGetter $currency;
|
||||
|
||||
/**
|
||||
* 2-letter country code of the shop.
|
||||
|
@ -46,15 +48,15 @@ class ApmApplies {
|
|||
/**
|
||||
* DccApplies constructor.
|
||||
*
|
||||
* @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.
|
||||
* @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 CurrencyGetter $currency The getter of the 3-letter currency code of the shop.
|
||||
* @param string $country 2-letter country code of the shop.
|
||||
*/
|
||||
public function __construct(
|
||||
array $allowed_countries,
|
||||
array $allowed_currencies,
|
||||
string $currency,
|
||||
CurrencyGetter $currency,
|
||||
string $country
|
||||
) {
|
||||
$this->allowed_countries = $allowed_countries;
|
||||
|
@ -78,7 +80,7 @@ class ApmApplies {
|
|||
* @return bool
|
||||
*/
|
||||
public function for_currency(): bool {
|
||||
return in_array( $this->currency, $this->allowed_currencies, true );
|
||||
return in_array( $this->currency->get(), $this->allowed_currencies, true );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
14
modules/ppcp-axo-block/.babelrc
Normal file
14
modules/ppcp-axo-block/.babelrc
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": "3.25.0"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/preset-react"
|
||||
]
|
||||
]
|
||||
}
|
17
modules/ppcp-axo-block/composer.json
Normal file
17
modules/ppcp-axo-block/composer.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "woocommerce/ppcp-axo-block",
|
||||
"type": "dhii-mod",
|
||||
"description": "Axo Block module for PPCP",
|
||||
"license": "GPL-2.0",
|
||||
"require": {
|
||||
"php": "^7.2 | ^8.0",
|
||||
"dhii/module-interface": "^0.3.0-alpha1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WooCommerce\\PayPalCommerce\\AxoBlock\\": "src"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
12
modules/ppcp-axo-block/extensions.php
Normal file
12
modules/ppcp-axo-block/extensions.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
/**
|
||||
* The Axo Block module extensions.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\AxoBlock
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\AxoBlock;
|
||||
|
||||
return array();
|
14
modules/ppcp-axo-block/module.php
Normal file
14
modules/ppcp-axo-block/module.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
/**
|
||||
* The Axo Block module.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\AxoBlock
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\AxoBlock;
|
||||
|
||||
return static function () : AxoBlockModule {
|
||||
return new AxoBlockModule();
|
||||
};
|
7142
modules/ppcp-axo-block/package-lock.json
generated
Normal file
7142
modules/ppcp-axo-block/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
42
modules/ppcp-axo-block/package.json
Normal file
42
modules/ppcp-axo-block/package.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "ppcp-axo-block",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"browserslist": [
|
||||
"> 0.5%",
|
||||
"Safari >= 8",
|
||||
"Chrome >= 41",
|
||||
"Firefox >= 43",
|
||||
"Edge >= 14"
|
||||
],
|
||||
"dependencies": {
|
||||
"@paypal/paypal-js": "^8.1.1",
|
||||
"@paypal/react-paypal-js": "^8.5.0",
|
||||
"core-js": "^3.25.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19",
|
||||
"@babel/preset-env": "^7.19",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
|
||||
"babel-loader": "^8.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"sass": "^1.42.1",
|
||||
"sass-loader": "^12.1.0",
|
||||
"webpack": "^5.76",
|
||||
"webpack-cli": "^4.10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
|
||||
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
|
||||
"dev": "cross-env BABEL_ENV=default webpack --watch"
|
||||
},
|
||||
"resolve": {
|
||||
"alias": {
|
||||
"@woocommerce/base-context": "assets/js/base/context"
|
||||
}
|
||||
}
|
||||
}
|
310
modules/ppcp-axo-block/resources/css/gateway.scss
Normal file
310
modules/ppcp-axo-block/resources/css/gateway.scss
Normal file
|
@ -0,0 +1,310 @@
|
|||
// Variables
|
||||
$border-color: hsla(0, 0%, 7%, 0.11);
|
||||
$transition-duration: 0.3s;
|
||||
$fast-transition-duration: 0.5s;
|
||||
|
||||
// Mixins
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@mixin flex-space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 1. AXO Block Radio Label
|
||||
#ppcp-axo-block-radio-label {
|
||||
@include flex-space-between;
|
||||
width: 100%;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
// 2. AXO Block Card
|
||||
.wc-block-checkout-axo-block-card {
|
||||
@include flex-center;
|
||||
width: 100%;
|
||||
margin-bottom: 2em;
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__content {
|
||||
box-sizing: border-box;
|
||||
aspect-ratio: 1.586;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid $border-color;
|
||||
font-size: 0.875em;
|
||||
font-family: monospace;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
@include flex-space-between;
|
||||
width: 100%;
|
||||
|
||||
&-digits {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&__watermark {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&__edit {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
margin: 0 0 0 auto;
|
||||
font-size: 0.875em;
|
||||
font-weight: normal;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-axo-block-card__meta-icon {
|
||||
max-height: 25px;
|
||||
}
|
||||
|
||||
// 3. Express Payment Block
|
||||
.wp-block-woocommerce-checkout-express-payment-block {
|
||||
transition: opacity $transition-duration ease-in,
|
||||
scale $transition-duration ease-in,
|
||||
display $transition-duration ease-in;
|
||||
transition-behavior: allow-discrete;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&.wc-block-axo-is-authenticated {
|
||||
opacity: 0;
|
||||
scale: 0.9;
|
||||
display: none !important;
|
||||
transition-duration: $fast-transition-duration;
|
||||
transition-timing-function: var(--ease-out-5);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. AXO Loaded State
|
||||
.wc-block-axo-is-loaded {
|
||||
// 4.1 Text Input
|
||||
.wc-block-components-text-input {
|
||||
display: flex;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
// 4.2 Hidden Fields
|
||||
&:not(.wc-block-axo-email-lookup-completed) {
|
||||
#shipping-fields,
|
||||
#billing-fields,
|
||||
#shipping-option,
|
||||
#order-notes,
|
||||
.wp-block-woocommerce-checkout-terms-block,
|
||||
.wp-block-woocommerce-checkout-actions-block {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 4.3 Authenticated State
|
||||
&.wc-block-axo-is-authenticated .wc-block-components-text-input {
|
||||
gap: 14px 0;
|
||||
}
|
||||
|
||||
// 4.4 Contact Information Block
|
||||
.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"input"
|
||||
"button"
|
||||
"watermark"
|
||||
"error";
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
align-items: start;
|
||||
|
||||
input[type="email"] {
|
||||
grid-area: input;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#email {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
// 4.5 Email Submit Button
|
||||
.wc-block-axo-email-submit-button-container {
|
||||
grid-area: button;
|
||||
align-self: stretch;
|
||||
|
||||
.wc-block-components-button {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 4.6 Watermark Container
|
||||
.wc-block-checkout-axo-block-watermark-container {
|
||||
grid-area: watermark;
|
||||
justify-self: end;
|
||||
grid-column: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// 4.7 Validation Error
|
||||
.wc-block-components-address-form__email .wc-block-components-validation-error {
|
||||
grid-area: error;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
grid-row: 3;
|
||||
|
||||
@media (min-width: 783px) {
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 783px) {
|
||||
.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input {
|
||||
grid-template-areas:
|
||||
"input button"
|
||||
"watermark watermark"
|
||||
"error error";
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 6px 8px;
|
||||
}
|
||||
|
||||
#email {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.wc-block-axo-email-submit-button-container {
|
||||
align-self: center;
|
||||
|
||||
.wc-block-components-button {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.8 Counter fix
|
||||
.wc-block-checkout__form {
|
||||
counter-reset: visible-step;
|
||||
|
||||
.wc-block-components-checkout-step--with-step-number {
|
||||
counter-increment: visible-step;
|
||||
|
||||
.wc-block-components-checkout-step__title:before {
|
||||
content: counter(visible-step) ". ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Shipping/Card Change Link
|
||||
a.wc-block-axo-change-link {
|
||||
color: var(--wp--preset--color--secondary);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-decoration: underline dashed;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--wp--preset--color--secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Watermark Container
|
||||
.wc-block-checkout-axo-block-watermark-container {
|
||||
height: 25px;
|
||||
margin-top: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
// 7. Checkout Fields Block (AXO Not Loaded)
|
||||
.wp-block-woocommerce-checkout-fields-block:not(.wc-block-axo-is-loaded) {
|
||||
.wc-block-checkout-axo-block-watermark-container {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
margin-right: 10px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.wc-block-components-spinner {
|
||||
box-sizing: content-box;
|
||||
color: inherit;
|
||||
font-size: 1em;
|
||||
height: auto;
|
||||
width: auto;
|
||||
position: relative;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. AXO Loaded Contact Information Block
|
||||
.wc-block-axo-is-loaded .wp-block-woocommerce-checkout-contact-information-block {
|
||||
.wc-block-checkout-axo-block-watermark-container .wc-block-components-spinner {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Transitions
|
||||
.wc-block-axo-email-submit-button-container,
|
||||
.wc-block-checkout-axo-block-watermark-container #fastlane-watermark-email,
|
||||
a.wc-block-axo-change-link {
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
scale: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Shipping Fields
|
||||
#shipping-fields .wc-block-components-checkout-step__heading {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// 11. Fastlane modal info message fix
|
||||
.wc-block-components-text-input {
|
||||
.wc-block-components-form &,
|
||||
& {
|
||||
paypal-watermark {
|
||||
white-space: wrap;
|
||||
}
|
||||
}
|
||||
}
|
72
modules/ppcp-axo-block/resources/js/components/Card/Card.js
Normal file
72
modules/ppcp-axo-block/resources/js/components/Card/Card.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { useMemo } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { Watermark } from '../Watermark';
|
||||
import { STORE_NAME } from '../../stores/axoStore';
|
||||
|
||||
const cardIcons = {
|
||||
VISA: 'visa-light.svg',
|
||||
MASTERCARD: 'mastercard-light.svg',
|
||||
AMEX: 'amex-light.svg',
|
||||
DISCOVER: 'discover-light.svg',
|
||||
DINERS: 'dinersclub-light.svg',
|
||||
JCB: 'jcb-light.svg',
|
||||
UNIONPAY: 'unionpay-light.svg',
|
||||
};
|
||||
|
||||
const Card = ( { fastlaneSdk, showWatermark = true } ) => {
|
||||
const { card } = useSelect(
|
||||
( select ) => ( {
|
||||
card: select( STORE_NAME ).getCardDetails(),
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
const { brand, lastDigits, expiry, name } = card?.paymentSource?.card ?? {};
|
||||
|
||||
const cardLogo = useMemo( () => {
|
||||
return cardIcons[ brand ] ? (
|
||||
<img
|
||||
className="wc-block-axo-block-card__meta-icon"
|
||||
title={ brand }
|
||||
src={ `${ window.wc_ppcp_axo.icons_directory }${ cardIcons[ brand ] }` }
|
||||
alt={ brand }
|
||||
/>
|
||||
) : (
|
||||
<span>{ brand }</span>
|
||||
);
|
||||
}, [ brand ] );
|
||||
|
||||
const formattedExpiry = expiry
|
||||
? `${ expiry.split( '-' )[ 1 ] }/${ expiry.split( '-' )[ 0 ] }`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className="wc-block-checkout-axo-block-card">
|
||||
<div className="wc-block-checkout-axo-block-card__inner">
|
||||
<div className="wc-block-checkout-axo-block-card__content">
|
||||
<div className="wc-block-checkout-axo-block-card__meta">
|
||||
<span className="wc-block-checkout-axo-block-card__meta-digits">
|
||||
{ `**** **** **** ${ lastDigits }` }
|
||||
</span>
|
||||
{ cardLogo }
|
||||
</div>
|
||||
<div className="wc-block-checkout-axo-block-card__meta">
|
||||
<span>{ name }</span>
|
||||
<span>{ formattedExpiry }</span>{ ' ' }
|
||||
</div>
|
||||
</div>
|
||||
<div className="wc-block-checkout-axo-block-card__watermark">
|
||||
{ showWatermark && (
|
||||
<Watermark
|
||||
fastlaneSdk={ fastlaneSdk }
|
||||
name="wc-block-checkout-axo-card-watermark"
|
||||
includeAdditionalInfo={ false }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
|
@ -0,0 +1,28 @@
|
|||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Renders a button to change the selected card in the checkout process.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onChangeButtonClick - Callback function to handle the click event.
|
||||
* @return {JSX.Element} The rendered button as an anchor tag.
|
||||
*/
|
||||
const CardChangeButton = ( { onChangeButtonClick } ) =>
|
||||
createElement(
|
||||
'a',
|
||||
{
|
||||
className:
|
||||
'wc-block-checkout-axo-block-card__edit wc-block-axo-change-link',
|
||||
role: 'button',
|
||||
onClick: ( event ) => {
|
||||
// Prevent default anchor behavior
|
||||
event.preventDefault();
|
||||
// Call the provided click handler
|
||||
onChangeButtonClick();
|
||||
},
|
||||
},
|
||||
__( 'Choose a different card', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
|
||||
export default CardChangeButton;
|
|
@ -0,0 +1,51 @@
|
|||
import { createElement, createRoot, useEffect } from '@wordpress/element';
|
||||
import CardChangeButton from './CardChangeButton';
|
||||
|
||||
/**
|
||||
* Manages the insertion and removal of the CardChangeButton in the DOM.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onChangeButtonClick - Callback function for when the card change button is clicked.
|
||||
* @return {null} This component doesn't render any visible elements directly.
|
||||
*/
|
||||
const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
|
||||
useEffect( () => {
|
||||
const radioLabelElement = document.getElementById(
|
||||
'ppcp-axo-block-radio-label'
|
||||
);
|
||||
|
||||
if ( radioLabelElement ) {
|
||||
// Check if the change button doesn't already exist
|
||||
if (
|
||||
! radioLabelElement.querySelector(
|
||||
'.wc-block-checkout-axo-block-card__edit'
|
||||
)
|
||||
) {
|
||||
// Create a new container for the button
|
||||
const buttonContainer = document.createElement( 'div' );
|
||||
radioLabelElement.appendChild( buttonContainer );
|
||||
|
||||
// Create a React root and render the CardChangeButton
|
||||
const root = createRoot( buttonContainer );
|
||||
root.render(
|
||||
createElement( CardChangeButton, { onChangeButtonClick } )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function to remove the button when the component unmounts
|
||||
return () => {
|
||||
const button = document.querySelector(
|
||||
'.wc-block-checkout-axo-block-card__edit'
|
||||
);
|
||||
if ( button && button.parentNode ) {
|
||||
button.parentNode.remove();
|
||||
}
|
||||
};
|
||||
}, [ onChangeButtonClick ] );
|
||||
|
||||
// This component doesn't render anything directly
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CardChangeButtonManager;
|
|
@ -0,0 +1,4 @@
|
|||
export { default as Card } from './Card';
|
||||
export { default as CardChangeButton } from './CardChangeButton';
|
||||
export { default as CardChangeButtonManager } from './CardChangeButtonManager';
|
||||
export { injectCardChangeButton, removeCardChangeButton } from './utils';
|
32
modules/ppcp-axo-block/resources/js/components/Card/utils.js
Normal file
32
modules/ppcp-axo-block/resources/js/components/Card/utils.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { createElement, createRoot } from '@wordpress/element';
|
||||
import CardChangeButtonManager from './CardChangeButtonManager';
|
||||
|
||||
/**
|
||||
* Injects a card change button into the DOM.
|
||||
*
|
||||
* @param {Function} onChangeButtonClick - Callback function for when the card change button is clicked.
|
||||
*/
|
||||
export const injectCardChangeButton = ( onChangeButtonClick ) => {
|
||||
// Create a container for the button
|
||||
const container = document.createElement( 'div' );
|
||||
document.body.appendChild( container );
|
||||
|
||||
// Render the CardChangeButtonManager in the new container
|
||||
createRoot( container ).render(
|
||||
createElement( CardChangeButtonManager, { onChangeButtonClick } )
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the card change button from the DOM if it exists.
|
||||
*/
|
||||
export const removeCardChangeButton = () => {
|
||||
const button = document.querySelector(
|
||||
'.wc-block-checkout-axo-block-card__edit'
|
||||
);
|
||||
|
||||
// Remove the button's parent node if it exists
|
||||
if ( button && button.parentNode ) {
|
||||
button.parentNode.remove();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
import { STORE_NAME } from '../../stores/axoStore';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Renders a submit button for email input in the AXO checkout process.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.handleSubmit - Function to handle button click/submit.
|
||||
* @return {JSX.Element|null} The rendered button or null if conditions are not met.
|
||||
*/
|
||||
const EmailButton = ( { handleSubmit } ) => {
|
||||
// Select relevant states from the AXO store
|
||||
const { isGuest, isAxoActive, isEmailSubmitted } = useSelect(
|
||||
( select ) => ( {
|
||||
isGuest: select( STORE_NAME ).getIsGuest(),
|
||||
isAxoActive: select( STORE_NAME ).getIsAxoActive(),
|
||||
isEmailSubmitted: select( STORE_NAME ).getIsEmailSubmitted(),
|
||||
} )
|
||||
);
|
||||
|
||||
// Only render the button for guests when AXO is active
|
||||
if ( ! isGuest || ! isAxoActive ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={ handleSubmit }
|
||||
className={ `wc-block-components-button wp-element-button ${
|
||||
isEmailSubmitted ? 'is-loading' : ''
|
||||
}` }
|
||||
disabled={ isEmailSubmitted }
|
||||
>
|
||||
{ /* Button text */ }
|
||||
<span
|
||||
className="wc-block-components-button__text"
|
||||
style={ {
|
||||
visibility: isEmailSubmitted ? 'hidden' : 'visible',
|
||||
} }
|
||||
>
|
||||
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
|
||||
</span>
|
||||
{ /* Loading spinner */ }
|
||||
{ isEmailSubmitted && (
|
||||
<span
|
||||
className="wc-block-components-spinner"
|
||||
aria-hidden="true"
|
||||
style={ {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailButton;
|
|
@ -0,0 +1,6 @@
|
|||
export { default as EmailButton } from './EmailButton';
|
||||
export {
|
||||
setupEmailFunctionality,
|
||||
removeEmailFunctionality,
|
||||
isEmailFunctionalitySetup,
|
||||
} from './utils';
|
|
@ -0,0 +1,147 @@
|
|||
import { createElement, createRoot } from '@wordpress/element';
|
||||
import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { STORE_NAME } from '../../stores/axoStore';
|
||||
import EmailButton from './EmailButton';
|
||||
|
||||
// Cache for DOM elements and references
|
||||
let emailInput = null;
|
||||
let submitButtonReference = {
|
||||
container: null,
|
||||
root: null,
|
||||
unsubscribe: null,
|
||||
};
|
||||
let keydownHandler = null;
|
||||
|
||||
/**
|
||||
* Retrieves or caches the email input element.
|
||||
*
|
||||
* @return {HTMLElement|null} The email input element or null if not found.
|
||||
*/
|
||||
const getEmailInput = () => {
|
||||
if ( ! emailInput ) {
|
||||
emailInput = document.getElementById( 'email' );
|
||||
}
|
||||
return emailInput;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up email functionality for AXO checkout.
|
||||
*
|
||||
* @param {Function} onEmailSubmit - Callback function to handle email submission.
|
||||
*/
|
||||
export const setupEmailFunctionality = ( onEmailSubmit ) => {
|
||||
const input = getEmailInput();
|
||||
if ( ! input ) {
|
||||
log(
|
||||
'Email input element not found. Functionality not added.',
|
||||
'warn'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handler for email submission
|
||||
const handleEmailSubmit = async () => {
|
||||
const isEmailSubmitted = wp.data
|
||||
.select( STORE_NAME )
|
||||
.getIsEmailSubmitted();
|
||||
|
||||
if ( isEmailSubmitted || ! input.value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp.data.dispatch( STORE_NAME ).setIsEmailSubmitted( true );
|
||||
renderButton();
|
||||
|
||||
try {
|
||||
await onEmailSubmit( input.value );
|
||||
} catch ( error ) {
|
||||
log( `Error during email submission: ${ error }`, 'error' );
|
||||
} finally {
|
||||
wp.data.dispatch( STORE_NAME ).setIsEmailSubmitted( false );
|
||||
renderButton();
|
||||
}
|
||||
};
|
||||
|
||||
// Set up keydown handler for Enter key
|
||||
keydownHandler = ( event ) => {
|
||||
const isAxoActive = wp.data.select( STORE_NAME ).getIsAxoActive();
|
||||
if ( event.key === 'Enter' && isAxoActive ) {
|
||||
event.preventDefault();
|
||||
handleEmailSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener( 'keydown', keydownHandler );
|
||||
|
||||
// Set up submit button
|
||||
if ( ! submitButtonReference.container ) {
|
||||
submitButtonReference.container = document.createElement( 'div' );
|
||||
submitButtonReference.container.setAttribute(
|
||||
'class',
|
||||
'wc-block-axo-email-submit-button-container'
|
||||
);
|
||||
|
||||
input.parentNode.insertBefore(
|
||||
submitButtonReference.container,
|
||||
input.nextSibling
|
||||
);
|
||||
|
||||
submitButtonReference.root = createRoot(
|
||||
submitButtonReference.container
|
||||
);
|
||||
}
|
||||
|
||||
// Function to render the EmailButton
|
||||
const renderButton = () => {
|
||||
if ( submitButtonReference.root ) {
|
||||
submitButtonReference.root.render(
|
||||
createElement( EmailButton, {
|
||||
handleSubmit: handleEmailSubmit,
|
||||
} )
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderButton();
|
||||
|
||||
// Subscribe to state changes and re-render button
|
||||
submitButtonReference.unsubscribe = wp.data.subscribe( () => {
|
||||
renderButton();
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes email functionality and cleans up event listeners and DOM elements.
|
||||
*/
|
||||
export const removeEmailFunctionality = () => {
|
||||
const input = getEmailInput();
|
||||
if ( input && keydownHandler ) {
|
||||
input.removeEventListener( 'keydown', keydownHandler );
|
||||
}
|
||||
|
||||
if ( submitButtonReference.root ) {
|
||||
submitButtonReference.root.unmount();
|
||||
}
|
||||
if ( submitButtonReference.unsubscribe ) {
|
||||
submitButtonReference.unsubscribe();
|
||||
}
|
||||
if (
|
||||
submitButtonReference.container &&
|
||||
submitButtonReference.container.parentNode
|
||||
) {
|
||||
submitButtonReference.container.parentNode.removeChild(
|
||||
submitButtonReference.container
|
||||
);
|
||||
}
|
||||
submitButtonReference = { container: null, root: null, unsubscribe: null };
|
||||
keydownHandler = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if email functionality is currently set up.
|
||||
*
|
||||
* @return {boolean} True if email functionality is set up, false otherwise.
|
||||
*/
|
||||
export const isEmailFunctionalitySetup = () => {
|
||||
return !! submitButtonReference.root;
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
import { useEffect, useCallback, useState } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { Card } from '../Card';
|
||||
import { STORE_NAME } from '../../stores/axoStore';
|
||||
|
||||
/**
|
||||
* Renders the payment component based on the user's state (guest or authenticated).
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.fastlaneSdk - The Fastlane SDK instance.
|
||||
* @param {Function} props.onPaymentLoad - Callback function when payment component is loaded.
|
||||
* @return {JSX.Element} The rendered payment component.
|
||||
*/
|
||||
export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => {
|
||||
const [ isCardElementReady, setIsCardElementReady ] = useState( false );
|
||||
|
||||
// Select relevant states from the AXO store
|
||||
const { isGuest, isEmailLookupCompleted, cardDetails } = useSelect(
|
||||
( select ) => ( {
|
||||
isGuest: select( STORE_NAME ).getIsGuest(),
|
||||
isEmailLookupCompleted:
|
||||
select( STORE_NAME ).getIsEmailLookupCompleted(),
|
||||
cardDetails: select( STORE_NAME ).getCardDetails(),
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Loads and renders the Fastlane card fields component when necessary.
|
||||
* This function is called for:
|
||||
* 1. Guest users who have completed email lookup
|
||||
* 2. Authenticated users who are missing card details
|
||||
*
|
||||
* The component allows users to enter new card details for payment.
|
||||
*/
|
||||
const loadPaymentComponent = useCallback( async () => {
|
||||
if (
|
||||
( isGuest && isEmailLookupCompleted && isCardElementReady ) ||
|
||||
( ! isGuest && ! cardDetails )
|
||||
) {
|
||||
try {
|
||||
const paymentComponent =
|
||||
await fastlaneSdk.FastlaneCardComponent( {} );
|
||||
// Check if the container exists before rendering
|
||||
const cardContainer =
|
||||
document.querySelector( '#fastlane-card' );
|
||||
if ( cardContainer ) {
|
||||
paymentComponent.render( '#fastlane-card' );
|
||||
onPaymentLoad( paymentComponent );
|
||||
}
|
||||
} catch ( error ) {
|
||||
log( `Error loading payment component: ${ error }`, 'error' );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isGuest,
|
||||
isEmailLookupCompleted,
|
||||
isCardElementReady,
|
||||
cardDetails,
|
||||
fastlaneSdk,
|
||||
onPaymentLoad,
|
||||
] );
|
||||
|
||||
// Set card element ready when guest email lookup is completed
|
||||
useEffect( () => {
|
||||
if ( isGuest && isEmailLookupCompleted ) {
|
||||
setIsCardElementReady( true );
|
||||
}
|
||||
}, [ isGuest, isEmailLookupCompleted ] );
|
||||
|
||||
// Load payment component when card element is ready
|
||||
useEffect( () => {
|
||||
if ( isCardElementReady ) {
|
||||
loadPaymentComponent();
|
||||
}
|
||||
}, [ isCardElementReady, loadPaymentComponent ] );
|
||||
|
||||
/**
|
||||
* Determines which component to render based on the current state.
|
||||
*
|
||||
* Rendering logic:
|
||||
* 1. For guests without completed email lookup: Show message to enter email
|
||||
* 2. For guests with completed email lookup: Render Fastlane card fields
|
||||
* 3. For authenticated users without card details: Render Fastlane card fields
|
||||
* 4. For authenticated users with card details: Render Card component
|
||||
*
|
||||
* @return {JSX.Element} The appropriate component based on the current state
|
||||
*/
|
||||
const renderPaymentComponent = () => {
|
||||
// Case 1: Guest user without completed email lookup
|
||||
if ( isGuest && ! isEmailLookupCompleted ) {
|
||||
return (
|
||||
<div id="ppcp-axo-block-radio-content">
|
||||
{ __(
|
||||
'Enter your email address above to continue.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Case 2 & 3: Guest with completed email lookup or authenticated user without card details
|
||||
if (
|
||||
( isGuest && isEmailLookupCompleted ) ||
|
||||
( ! isGuest && ! cardDetails )
|
||||
) {
|
||||
return <div id="fastlane-card" />;
|
||||
}
|
||||
|
||||
// Case 4: Authenticated user with card details
|
||||
return <Card fastlaneSdk={ fastlaneSdk } showWatermark={ ! isGuest } />;
|
||||
};
|
||||
|
||||
return renderPaymentComponent();
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Renders a button to change the shipping address.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onChangeShippingAddressClick - Callback function to handle the click event.
|
||||
* @return {JSX.Element} The rendered button as an anchor tag.
|
||||
*/
|
||||
const ShippingChangeButton = ( { onChangeShippingAddressClick } ) => (
|
||||
<a
|
||||
className="wc-block-axo-change-link"
|
||||
role="button"
|
||||
onClick={ ( event ) => {
|
||||
// Prevent default anchor behavior
|
||||
event.preventDefault();
|
||||
// Call the provided click handler
|
||||
onChangeShippingAddressClick();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Choose a different shipping address',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</a>
|
||||
);
|
||||
|
||||
export default ShippingChangeButton;
|
|
@ -0,0 +1,51 @@
|
|||
import { useEffect, createRoot } from '@wordpress/element';
|
||||
import ShippingChangeButton from './ShippingChangeButton';
|
||||
|
||||
/**
|
||||
* Manages the insertion and removal of the ShippingChangeButton in the DOM.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onChangeShippingAddressClick - Callback function for when the shipping change button is clicked.
|
||||
* @return {null} This component doesn't render any visible elements directly.
|
||||
*/
|
||||
const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => {
|
||||
useEffect( () => {
|
||||
const shippingHeading = document.querySelector(
|
||||
'#shipping-fields .wc-block-components-checkout-step__heading'
|
||||
);
|
||||
|
||||
// Check if the shipping heading exists and doesn't already have a change button
|
||||
if (
|
||||
shippingHeading &&
|
||||
! shippingHeading.querySelector(
|
||||
'.wc-block-checkout-axo-block-card__edit'
|
||||
)
|
||||
) {
|
||||
// Create a new span element to contain the ShippingChangeButton
|
||||
const spanElement = document.createElement( 'span' );
|
||||
spanElement.className = 'wc-block-checkout-axo-block-card__edit';
|
||||
shippingHeading.appendChild( spanElement );
|
||||
|
||||
// Create a React root and render the ShippingChangeButton
|
||||
const root = createRoot( spanElement );
|
||||
root.render(
|
||||
<ShippingChangeButton
|
||||
onChangeShippingAddressClick={
|
||||
onChangeShippingAddressClick
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
// Cleanup function to remove the button when the component unmounts
|
||||
return () => {
|
||||
root.unmount();
|
||||
spanElement.remove();
|
||||
};
|
||||
}
|
||||
}, [ onChangeShippingAddressClick ] );
|
||||
|
||||
// This component doesn't render anything directly
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ShippingChangeButtonManager;
|
|
@ -0,0 +1,6 @@
|
|||
export { default as ShippingChangeButton } from './ShippingChangeButton';
|
||||
export { default as ShippingChangeButtonManager } from './ShippingChangeButtonManager';
|
||||
export {
|
||||
injectShippingChangeButton,
|
||||
removeShippingChangeButton,
|
||||
} from './utils';
|
|
@ -0,0 +1,40 @@
|
|||
import { createRoot } from '@wordpress/element';
|
||||
import ShippingChangeButtonManager from './ShippingChangeButtonManager';
|
||||
|
||||
/**
|
||||
* Injects a shipping change button into the DOM if it doesn't already exist.
|
||||
*
|
||||
* @param {Function} onChangeShippingAddressClick - Callback function for when the shipping change button is clicked.
|
||||
*/
|
||||
export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => {
|
||||
// Check if the button already exists
|
||||
const existingButton = document.querySelector(
|
||||
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
|
||||
);
|
||||
|
||||
if ( ! existingButton ) {
|
||||
// Create a new container for the button
|
||||
const container = document.createElement( 'div' );
|
||||
document.body.appendChild( container );
|
||||
|
||||
// Render the ShippingChangeButtonManager in the new container
|
||||
createRoot( container ).render(
|
||||
<ShippingChangeButtonManager
|
||||
onChangeShippingAddressClick={ onChangeShippingAddressClick }
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the shipping change button from the DOM if it exists.
|
||||
*/
|
||||
export const removeShippingChangeButton = () => {
|
||||
const span = document.querySelector(
|
||||
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
|
||||
);
|
||||
if ( span ) {
|
||||
createRoot( span ).unmount();
|
||||
span.remove();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
|
||||
/**
|
||||
* Watermark component for displaying AXO watermark.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.fastlaneSdk - The Fastlane SDK instance.
|
||||
* @param {string} [props.name='fastlane-watermark-container'] - ID for the watermark container.
|
||||
* @param {boolean} [props.includeAdditionalInfo=true] - Whether to include additional info in the watermark.
|
||||
* @return {JSX.Element} The watermark container element.
|
||||
*/
|
||||
const Watermark = ( {
|
||||
fastlaneSdk,
|
||||
name = 'fastlane-watermark-container',
|
||||
includeAdditionalInfo = true,
|
||||
} ) => {
|
||||
const containerRef = useRef( null );
|
||||
const watermarkRef = useRef( null );
|
||||
|
||||
useEffect( () => {
|
||||
/**
|
||||
* Renders the Fastlane watermark.
|
||||
*/
|
||||
const renderWatermark = async () => {
|
||||
if ( ! containerRef.current ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the container before rendering
|
||||
containerRef.current.innerHTML = '';
|
||||
|
||||
try {
|
||||
// Create and render the Fastlane watermark
|
||||
const watermark = await fastlaneSdk.FastlaneWatermarkComponent(
|
||||
{
|
||||
includeAdditionalInfo,
|
||||
}
|
||||
);
|
||||
|
||||
watermarkRef.current = watermark;
|
||||
watermark.render( `#${ name }` );
|
||||
} catch ( error ) {
|
||||
log( `Error rendering watermark: ${ error }`, 'error' );
|
||||
}
|
||||
};
|
||||
|
||||
renderWatermark();
|
||||
|
||||
// Cleanup function to clear the container on unmount
|
||||
return () => {
|
||||
if ( containerRef.current ) {
|
||||
containerRef.current.innerHTML = '';
|
||||
}
|
||||
};
|
||||
}, [ fastlaneSdk, name, includeAdditionalInfo ] );
|
||||
|
||||
// Render the container for the watermark
|
||||
return <div id={ name } ref={ containerRef } />;
|
||||
};
|
||||
|
||||
export default Watermark;
|
|
@ -0,0 +1,48 @@
|
|||
import { useEffect } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { STORE_NAME } from '../../stores/axoStore';
|
||||
import {
|
||||
createWatermarkContainer,
|
||||
removeWatermark,
|
||||
updateWatermarkContent,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* Manages the lifecycle and content of the AXO watermark.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.fastlaneSdk - The Fastlane SDK instance.
|
||||
* @return {null} This component doesn't render any visible elements.
|
||||
*/
|
||||
const WatermarkManager = ( { fastlaneSdk } ) => {
|
||||
// Select relevant states from the AXO store
|
||||
const isAxoActive = useSelect( ( select ) =>
|
||||
select( STORE_NAME ).getIsAxoActive()
|
||||
);
|
||||
const isAxoScriptLoaded = useSelect( ( select ) =>
|
||||
select( STORE_NAME ).getIsAxoScriptLoaded()
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( isAxoActive || ( ! isAxoActive && ! isAxoScriptLoaded ) ) {
|
||||
// Create watermark container and update content when AXO is active or loading
|
||||
createWatermarkContainer();
|
||||
updateWatermarkContent( {
|
||||
isAxoActive,
|
||||
isAxoScriptLoaded,
|
||||
fastlaneSdk,
|
||||
} );
|
||||
} else {
|
||||
// Remove watermark when AXO is inactive and not loading
|
||||
removeWatermark();
|
||||
}
|
||||
|
||||
// Cleanup function to remove watermark on unmount
|
||||
return removeWatermark;
|
||||
}, [ fastlaneSdk, isAxoActive, isAxoScriptLoaded ] );
|
||||
|
||||
// This component doesn't render anything directly
|
||||
return null;
|
||||
};
|
||||
|
||||
export default WatermarkManager;
|
|
@ -0,0 +1,3 @@
|
|||
export { default as Watermark } from './Watermark';
|
||||
export { default as WatermarkManager } from './WatermarkManager';
|
||||
export { setupWatermark, removeWatermark } from './utils';
|
|
@ -0,0 +1,142 @@
|
|||
import { createElement, createRoot } from '@wordpress/element';
|
||||
import { Watermark, WatermarkManager } from '../Watermark';
|
||||
|
||||
// Object to store references to the watermark container and root
|
||||
const watermarkReference = {
|
||||
container: null,
|
||||
root: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a container for the watermark in the checkout contact information block.
|
||||
*/
|
||||
export const createWatermarkContainer = () => {
|
||||
const textInputContainer = document.querySelector(
|
||||
'.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input'
|
||||
);
|
||||
|
||||
if ( textInputContainer && ! watermarkReference.container ) {
|
||||
const emailInput =
|
||||
textInputContainer.querySelector( 'input[id="email"]' );
|
||||
|
||||
if ( emailInput ) {
|
||||
// Create watermark container
|
||||
watermarkReference.container = document.createElement( 'div' );
|
||||
watermarkReference.container.setAttribute(
|
||||
'class',
|
||||
'wc-block-checkout-axo-block-watermark-container'
|
||||
);
|
||||
|
||||
const emailButton = textInputContainer.querySelector(
|
||||
'.wc-block-axo-email-submit-button-container'
|
||||
);
|
||||
|
||||
// Insert the watermark after the "Continue" button or email input
|
||||
const insertAfterElement = emailButton || emailInput;
|
||||
|
||||
insertAfterElement.parentNode.insertBefore(
|
||||
watermarkReference.container,
|
||||
insertAfterElement.nextSibling
|
||||
);
|
||||
|
||||
// Create a root for the watermark
|
||||
watermarkReference.root = createRoot(
|
||||
watermarkReference.container
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up the watermark manager component.
|
||||
*
|
||||
* @param {Object} fastlaneSdk - The Fastlane SDK instance.
|
||||
* @return {Function} Cleanup function to remove the watermark.
|
||||
*/
|
||||
export const setupWatermark = ( fastlaneSdk ) => {
|
||||
const container = document.createElement( 'div' );
|
||||
document.body.appendChild( container );
|
||||
const root = createRoot( container );
|
||||
root.render( createElement( WatermarkManager, { fastlaneSdk } ) );
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
root.unmount();
|
||||
if ( container && container.parentNode ) {
|
||||
container.parentNode.removeChild( container );
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the watermark from the DOM and resets the reference.
|
||||
*/
|
||||
export const removeWatermark = () => {
|
||||
if ( watermarkReference.root ) {
|
||||
watermarkReference.root.unmount();
|
||||
}
|
||||
if ( watermarkReference.container ) {
|
||||
if ( watermarkReference.container.parentNode ) {
|
||||
watermarkReference.container.parentNode.removeChild(
|
||||
watermarkReference.container
|
||||
);
|
||||
} else {
|
||||
// Fallback removal if parent node is not available
|
||||
const detachedContainer = document.querySelector(
|
||||
'.wc-block-checkout-axo-block-watermark-container'
|
||||
);
|
||||
if ( detachedContainer ) {
|
||||
detachedContainer.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset watermark reference
|
||||
Object.assign( watermarkReference, { container: null, root: null } );
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders content in the watermark container.
|
||||
*
|
||||
* @param {ReactElement} content - The content to render.
|
||||
*/
|
||||
export const renderWatermarkContent = ( content ) => {
|
||||
if ( watermarkReference.root ) {
|
||||
watermarkReference.root.render( content );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the watermark content based on the current state.
|
||||
*
|
||||
* @param {Object} params - State parameters.
|
||||
* @param {boolean} params.isAxoActive - Whether AXO is active.
|
||||
* @param {boolean} params.isAxoScriptLoaded - Whether AXO script is loaded.
|
||||
* @param {Object} params.fastlaneSdk - The Fastlane SDK instance.
|
||||
*/
|
||||
export const updateWatermarkContent = ( {
|
||||
isAxoActive,
|
||||
isAxoScriptLoaded,
|
||||
fastlaneSdk,
|
||||
} ) => {
|
||||
if ( ! isAxoActive && ! isAxoScriptLoaded ) {
|
||||
// Show loading spinner
|
||||
renderWatermarkContent(
|
||||
createElement( 'span', {
|
||||
className: 'wc-block-components-spinner',
|
||||
'aria-hidden': 'true',
|
||||
} )
|
||||
);
|
||||
} else if ( isAxoActive ) {
|
||||
// Show Fastlane watermark
|
||||
renderWatermarkContent(
|
||||
createElement( Watermark, {
|
||||
fastlaneSdk,
|
||||
name: 'fastlane-watermark-email',
|
||||
includeAdditionalInfo: true,
|
||||
} )
|
||||
);
|
||||
} else {
|
||||
// Clear watermark content
|
||||
renderWatermarkContent( null );
|
||||
}
|
||||
};
|
119
modules/ppcp-axo-block/resources/js/events/emailLookupManager.js
Normal file
119
modules/ppcp-axo-block/resources/js/events/emailLookupManager.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { populateWooFields } from '../helpers/fieldHelpers';
|
||||
import { injectShippingChangeButton } from '../components/Shipping';
|
||||
import { injectCardChangeButton } from '../components/Card';
|
||||
import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore';
|
||||
|
||||
/**
|
||||
* Creates an email lookup handler function for AXO checkout.
|
||||
*
|
||||
* @param {Object} fastlaneSdk - The Fastlane SDK instance.
|
||||
* @param {Function} setShippingAddress - Function to set shipping address in the store.
|
||||
* @param {Function} setCardDetails - Function to set card details in the store.
|
||||
* @param {Function} snapshotFields - Function to save current field values.
|
||||
* @param {Object} wooShippingAddress - Current WooCommerce shipping address.
|
||||
* @param {Object} wooBillingAddress - Current WooCommerce billing address.
|
||||
* @param {Function} setWooShippingAddress - Function to update WooCommerce shipping address.
|
||||
* @param {Function} setWooBillingAddress - Function to update WooCommerce billing address.
|
||||
* @param {Function} onChangeShippingAddressClick - Handler for shipping address change.
|
||||
* @param {Function} onChangeCardButtonClick - Handler for card change.
|
||||
* @return {Function} The email lookup handler function.
|
||||
*/
|
||||
export const createEmailLookupHandler = (
|
||||
fastlaneSdk,
|
||||
setShippingAddress,
|
||||
setCardDetails,
|
||||
snapshotFields,
|
||||
wooShippingAddress,
|
||||
wooBillingAddress,
|
||||
setWooShippingAddress,
|
||||
setWooBillingAddress,
|
||||
onChangeShippingAddressClick,
|
||||
onChangeCardButtonClick
|
||||
) => {
|
||||
return async ( email ) => {
|
||||
try {
|
||||
log( `Email value being looked up: ${ email }` );
|
||||
|
||||
// Validate Fastlane SDK initialization
|
||||
if ( ! fastlaneSdk ) {
|
||||
throw new Error( 'FastlaneSDK is not initialized' );
|
||||
}
|
||||
|
||||
if ( ! fastlaneSdk.identity ) {
|
||||
throw new Error(
|
||||
'FastlaneSDK identity object is not available'
|
||||
);
|
||||
}
|
||||
|
||||
// Perform email lookup
|
||||
const lookup =
|
||||
await fastlaneSdk.identity.lookupCustomerByEmail( email );
|
||||
|
||||
log( `Lookup response: ${ JSON.stringify( lookup ) }` );
|
||||
|
||||
// Handle Gary flow (new user)
|
||||
if ( lookup && lookup.customerContextId === '' ) {
|
||||
setIsEmailLookupCompleted( true );
|
||||
}
|
||||
|
||||
if ( ! lookup || ! lookup.customerContextId ) {
|
||||
log( 'No customerContextId found in the response', 'warn' );
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger authentication flow
|
||||
const authResponse =
|
||||
await fastlaneSdk.identity.triggerAuthenticationFlow(
|
||||
lookup.customerContextId
|
||||
);
|
||||
|
||||
if ( ! authResponse || ! authResponse.authenticationState ) {
|
||||
throw new Error( 'Invalid authentication response' );
|
||||
}
|
||||
|
||||
const { authenticationState, profileData } = authResponse;
|
||||
|
||||
// Mark email lookup as completed for OTP flow
|
||||
if ( authResponse ) {
|
||||
setIsEmailLookupCompleted( true );
|
||||
}
|
||||
|
||||
// Handle successful authentication
|
||||
if ( authenticationState === 'succeeded' ) {
|
||||
// Save current field values
|
||||
snapshotFields( wooShippingAddress, wooBillingAddress );
|
||||
setIsGuest( false );
|
||||
|
||||
// Update store with profile data
|
||||
if ( profileData && profileData.shippingAddress ) {
|
||||
setShippingAddress( profileData.shippingAddress );
|
||||
}
|
||||
if ( profileData && profileData.card ) {
|
||||
setCardDetails( profileData.card );
|
||||
}
|
||||
|
||||
log( `Profile Data: ${ JSON.stringify( profileData ) }` );
|
||||
|
||||
// Populate WooCommerce fields with profile data
|
||||
populateWooFields(
|
||||
profileData,
|
||||
setWooShippingAddress,
|
||||
setWooBillingAddress
|
||||
);
|
||||
|
||||
// Inject change buttons for shipping and card
|
||||
injectShippingChangeButton( onChangeShippingAddressClick );
|
||||
injectCardChangeButton( onChangeCardButtonClick );
|
||||
} else {
|
||||
log( 'Authentication failed or did not succeed', 'warn' );
|
||||
}
|
||||
} catch ( error ) {
|
||||
log(
|
||||
`Error during email lookup or authentication:
|
||||
${ error }`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
165
modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js
Normal file
165
modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { select, subscribe } from '@wordpress/data';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
|
||||
/**
|
||||
* Sets up a class toggle based on the isGuest state for the express payment block.
|
||||
* This hides the express payment methods if the user is authenticated (Ryan flow).
|
||||
*
|
||||
* @return {Function} Unsubscribe function for cleanup.
|
||||
*/
|
||||
export const setupAuthenticationClassToggle = () => {
|
||||
const targetSelector =
|
||||
'.wp-block-woocommerce-checkout-express-payment-block';
|
||||
const authClass = 'wc-block-axo-is-authenticated';
|
||||
|
||||
const updateAuthenticationClass = () => {
|
||||
const targetElement = document.querySelector( targetSelector );
|
||||
if ( ! targetElement ) {
|
||||
log(
|
||||
`Authentication class target element not found: ${ targetSelector }`,
|
||||
'warn'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isGuest = select( STORE_NAME ).getIsGuest();
|
||||
|
||||
if ( ! isGuest ) {
|
||||
targetElement.classList.add( authClass );
|
||||
} else {
|
||||
targetElement.classList.remove( authClass );
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateAuthenticationClass();
|
||||
|
||||
// Subscribe to state changes
|
||||
const unsubscribe = subscribe( () => {
|
||||
updateAuthenticationClass();
|
||||
} );
|
||||
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up a class toggle based on the isEmailLookupCompleted state for the checkout fields block.
|
||||
* This hides the Shipping Address fields, Billing Address fields, Shipping Options section,
|
||||
* Order Notes section, Checkout Terms section, and Place Order button until email lookup is completed.
|
||||
*
|
||||
* @return {Function} Unsubscribe function for cleanup.
|
||||
*/
|
||||
export const setupEmailLookupCompletedClassToggle = () => {
|
||||
const targetSelector = '.wp-block-woocommerce-checkout-fields-block';
|
||||
const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed';
|
||||
|
||||
const updateEmailLookupCompletedClass = () => {
|
||||
const targetElement = document.querySelector( targetSelector );
|
||||
if ( ! targetElement ) {
|
||||
log(
|
||||
`Email lookup completed class target element not found: ${ targetSelector }`,
|
||||
'warn'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isEmailLookupCompleted =
|
||||
select( STORE_NAME ).getIsEmailLookupCompleted();
|
||||
|
||||
if ( isEmailLookupCompleted ) {
|
||||
targetElement.classList.add( emailLookupCompletedClass );
|
||||
} else {
|
||||
targetElement.classList.remove( emailLookupCompletedClass );
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateEmailLookupCompletedClass();
|
||||
|
||||
// Subscribe to state changes
|
||||
const unsubscribe = subscribe( () => {
|
||||
updateEmailLookupCompletedClass();
|
||||
} );
|
||||
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up class toggles for the contact information block based on isAxoActive, isGuest, and isEmailLookupCompleted states.
|
||||
* @return {Function} Unsubscribe function for cleanup.
|
||||
*/
|
||||
export const setupCheckoutBlockClassToggles = () => {
|
||||
const targetSelector = '.wp-block-woocommerce-checkout-fields-block';
|
||||
const axoLoadedClass = 'wc-block-axo-is-loaded';
|
||||
const authClass = 'wc-block-axo-is-authenticated';
|
||||
const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed';
|
||||
|
||||
const updateCheckoutBlockClassToggles = () => {
|
||||
const targetElement = document.querySelector( targetSelector );
|
||||
if ( ! targetElement ) {
|
||||
log(
|
||||
`Checkout block class target element not found: ${ targetSelector }`,
|
||||
'warn'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isAxoActive = select( STORE_NAME ).getIsAxoActive();
|
||||
const isGuest = select( STORE_NAME ).getIsGuest();
|
||||
const isEmailLookupCompleted =
|
||||
select( STORE_NAME ).getIsEmailLookupCompleted();
|
||||
|
||||
if ( isAxoActive ) {
|
||||
targetElement.classList.add( axoLoadedClass );
|
||||
} else {
|
||||
targetElement.classList.remove( axoLoadedClass );
|
||||
}
|
||||
|
||||
if ( ! isGuest ) {
|
||||
targetElement.classList.add( authClass );
|
||||
} else {
|
||||
targetElement.classList.remove( authClass );
|
||||
}
|
||||
|
||||
if ( isEmailLookupCompleted ) {
|
||||
targetElement.classList.add( emailLookupCompletedClass );
|
||||
} else {
|
||||
targetElement.classList.remove( emailLookupCompletedClass );
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateCheckoutBlockClassToggles();
|
||||
|
||||
// Subscribe to state changes
|
||||
const unsubscribe = subscribe( () => {
|
||||
updateCheckoutBlockClassToggles();
|
||||
} );
|
||||
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes all class toggles.
|
||||
* @return {Function} Cleanup function to unsubscribe all listeners.
|
||||
*/
|
||||
export const initializeClassToggles = () => {
|
||||
const unsubscribeAuth = setupAuthenticationClassToggle();
|
||||
const unsubscribeEmailLookupCompleted =
|
||||
setupEmailLookupCompletedClassToggle();
|
||||
const unsubscribeContactInfo = setupCheckoutBlockClassToggles();
|
||||
|
||||
// Return a cleanup function that unsubscribes all listeners
|
||||
return () => {
|
||||
if ( unsubscribeAuth ) {
|
||||
unsubscribeAuth();
|
||||
}
|
||||
if ( unsubscribeEmailLookupCompleted ) {
|
||||
unsubscribeEmailLookupCompleted();
|
||||
}
|
||||
if ( unsubscribeContactInfo ) {
|
||||
unsubscribeContactInfo();
|
||||
}
|
||||
};
|
||||
};
|
167
modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js
Normal file
167
modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { dispatch } from '@wordpress/data';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
|
||||
/**
|
||||
* Saves the current shipping and billing address to localStorage.
|
||||
*
|
||||
* @param {Object} shippingAddress - The current shipping address.
|
||||
* @param {Object} billingAddress - The current billing address.
|
||||
*/
|
||||
export const snapshotFields = ( shippingAddress, billingAddress ) => {
|
||||
if ( ! shippingAddress || ! billingAddress ) {
|
||||
log(
|
||||
`Shipping or billing address is missing: ${ JSON.stringify( {
|
||||
shippingAddress,
|
||||
billingAddress,
|
||||
} ) }`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
|
||||
const originalData = { shippingAddress, billingAddress };
|
||||
log( `Snapshot data: ${ JSON.stringify( originalData ) }` );
|
||||
try {
|
||||
// Save the original data to localStorage
|
||||
localStorage.setItem(
|
||||
'axoOriginalCheckoutFields',
|
||||
JSON.stringify( originalData )
|
||||
);
|
||||
} catch ( error ) {
|
||||
log( `Error saving to localStorage: ${ error }`, 'error' );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores the original shipping and billing addresses from localStorage.
|
||||
*
|
||||
* @param {Function} updateShippingAddress - Function to update the shipping address.
|
||||
* @param {Function} updateBillingAddress - Function to update the billing address.
|
||||
*/
|
||||
export const restoreOriginalFields = (
|
||||
updateShippingAddress,
|
||||
updateBillingAddress
|
||||
) => {
|
||||
log( 'Attempting to restore original fields' );
|
||||
let savedData;
|
||||
try {
|
||||
// Retrieve saved data from localStorage
|
||||
savedData = localStorage.getItem( 'axoOriginalCheckoutFields' );
|
||||
log(
|
||||
`Data retrieved from localStorage: ${ JSON.stringify( savedData ) }`
|
||||
);
|
||||
} catch ( error ) {
|
||||
log( `Error retrieving from localStorage: ${ error }`, 'error' );
|
||||
}
|
||||
|
||||
if ( savedData ) {
|
||||
try {
|
||||
const parsedData = JSON.parse( savedData );
|
||||
// Restore shipping address if available
|
||||
if ( parsedData.shippingAddress ) {
|
||||
updateShippingAddress( parsedData.shippingAddress );
|
||||
} else {
|
||||
log( `No shipping address found in saved data`, 'warn' );
|
||||
}
|
||||
// Restore billing address if available
|
||||
if ( parsedData.billingAddress ) {
|
||||
log(
|
||||
`Restoring billing address:
|
||||
${ JSON.stringify( parsedData.billingAddress ) }`
|
||||
);
|
||||
updateBillingAddress( parsedData.billingAddress );
|
||||
} else {
|
||||
log( 'No billing address found in saved data', 'warn' );
|
||||
}
|
||||
} catch ( error ) {
|
||||
log( `Error parsing saved data: ${ error }` );
|
||||
}
|
||||
} else {
|
||||
log(
|
||||
'No data found in localStorage under axoOriginalCheckoutFields',
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Populates WooCommerce fields with profile data from AXO.
|
||||
*
|
||||
* @param {Object} profileData - The profile data from AXO.
|
||||
* @param {Function} setWooShippingAddress - Function to set WooCommerce shipping address.
|
||||
* @param {Function} setWooBillingAddress - Function to set WooCommerce billing address.
|
||||
*/
|
||||
export const populateWooFields = (
|
||||
profileData,
|
||||
setWooShippingAddress,
|
||||
setWooBillingAddress
|
||||
) => {
|
||||
const CHECKOUT_STORE_KEY = 'wc/store/checkout';
|
||||
|
||||
log(
|
||||
`Populating WooCommerce fields with profile data: ${ JSON.stringify(
|
||||
profileData
|
||||
) }`
|
||||
);
|
||||
|
||||
const checkoutDispatch = dispatch( CHECKOUT_STORE_KEY );
|
||||
|
||||
// Uncheck the 'Use same address for billing' checkbox if the method exists
|
||||
if (
|
||||
typeof checkoutDispatch.__internalSetUseShippingAsBilling === 'function'
|
||||
) {
|
||||
checkoutDispatch.__internalSetUseShippingAsBilling( false );
|
||||
}
|
||||
|
||||
// Prepare and set shipping address
|
||||
const { address, name, phoneNumber } = profileData.shippingAddress;
|
||||
|
||||
const shippingAddress = {
|
||||
first_name: name.firstName,
|
||||
last_name: name.lastName,
|
||||
address_1: address.addressLine1,
|
||||
address_2: address.addressLine2 || '',
|
||||
city: address.adminArea2,
|
||||
state: address.adminArea1,
|
||||
postcode: address.postalCode,
|
||||
country: address.countryCode,
|
||||
phone: phoneNumber.nationalNumber,
|
||||
};
|
||||
|
||||
log(
|
||||
`Setting WooCommerce shipping address: ${ JSON.stringify(
|
||||
shippingAddress
|
||||
) }`
|
||||
);
|
||||
setWooShippingAddress( shippingAddress );
|
||||
|
||||
// Prepare and set billing address
|
||||
const billingData = profileData.card.paymentSource.card.billingAddress;
|
||||
|
||||
const billingAddress = {
|
||||
first_name: profileData.name.firstName,
|
||||
last_name: profileData.name.lastName,
|
||||
address_1: billingData.addressLine1,
|
||||
address_2: billingData.addressLine2 || '',
|
||||
city: billingData.adminArea2,
|
||||
state: billingData.adminArea1,
|
||||
postcode: billingData.postalCode,
|
||||
country: billingData.countryCode,
|
||||
};
|
||||
|
||||
log(
|
||||
`Setting WooCommerce billing address: ${ JSON.stringify(
|
||||
billingAddress
|
||||
) }`
|
||||
);
|
||||
setWooBillingAddress( billingAddress );
|
||||
|
||||
// Collapse shipping address input fields into the card view
|
||||
if ( typeof checkoutDispatch.setEditingShippingAddress === 'function' ) {
|
||||
checkoutDispatch.setEditingShippingAddress( false );
|
||||
}
|
||||
|
||||
// Collapse billing address input fields into the card view
|
||||
if ( typeof checkoutDispatch.setEditingBillingAddress === 'function' ) {
|
||||
checkoutDispatch.setEditingBillingAddress( false );
|
||||
}
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import { useCallback } from '@wordpress/element';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
|
||||
const CHECKOUT_STORE_KEY = 'wc/store/checkout';
|
||||
|
||||
/**
|
||||
* Custom hook to manage address editing states in the checkout process.
|
||||
*
|
||||
* When set to true (default), the shipping and billing address forms are displayed.
|
||||
* When set to false, the address forms are hidden and the user can only view the address details (card view).
|
||||
*
|
||||
* @return {Object} An object containing address editing states and setter functions.
|
||||
*/
|
||||
export const useAddressEditing = () => {
|
||||
// Select address editing states from the checkout store
|
||||
const { isEditingShippingAddress, isEditingBillingAddress } = useSelect(
|
||||
( select ) => {
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
return {
|
||||
// Default to true if the getter function doesn't exist
|
||||
isEditingShippingAddress: store.getEditingShippingAddress
|
||||
? store.getEditingShippingAddress()
|
||||
: true,
|
||||
isEditingBillingAddress: store.getEditingBillingAddress
|
||||
? store.getEditingBillingAddress()
|
||||
: true,
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Get dispatch functions to update address editing states
|
||||
const { setEditingShippingAddress, setEditingBillingAddress } =
|
||||
useDispatch( CHECKOUT_STORE_KEY );
|
||||
|
||||
// Memoized function to update shipping address editing state
|
||||
const setShippingAddressEditing = useCallback(
|
||||
( isEditing ) => {
|
||||
if ( typeof setEditingShippingAddress === 'function' ) {
|
||||
setEditingShippingAddress( isEditing );
|
||||
}
|
||||
},
|
||||
[ setEditingShippingAddress ]
|
||||
);
|
||||
|
||||
// Memoized function to update billing address editing state
|
||||
const setBillingAddressEditing = useCallback(
|
||||
( isEditing ) => {
|
||||
if ( typeof setEditingBillingAddress === 'function' ) {
|
||||
setEditingBillingAddress( isEditing );
|
||||
}
|
||||
},
|
||||
[ setEditingBillingAddress ]
|
||||
);
|
||||
|
||||
// Return an object with address editing states and setter functions
|
||||
return {
|
||||
isEditingShippingAddress,
|
||||
isEditingBillingAddress,
|
||||
setShippingAddressEditing,
|
||||
setBillingAddressEditing,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAddressEditing;
|
|
@ -0,0 +1,21 @@
|
|||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Custom hook returning the allowed shipping locations based on configuration.
|
||||
*
|
||||
* @param {Object} axoConfig - The AXO configuration object.
|
||||
* @param {Array|undefined} axoConfig.enabled_shipping_locations - The list of enabled shipping locations.
|
||||
* @return {Array} The final list of allowed shipping locations.
|
||||
*/
|
||||
const useAllowedLocations = ( axoConfig ) => {
|
||||
return useMemo( () => {
|
||||
const enabledShippingLocations =
|
||||
axoConfig.enabled_shipping_locations || [];
|
||||
|
||||
return Array.isArray( enabledShippingLocations )
|
||||
? enabledShippingLocations
|
||||
: [];
|
||||
}, [ axoConfig.enabled_shipping_locations ] );
|
||||
};
|
||||
|
||||
export default useAllowedLocations;
|
65
modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js
Normal file
65
modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { useEffect } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
import { removeShippingChangeButton } from '../components/Shipping';
|
||||
import { removeCardChangeButton } from '../components/Card';
|
||||
import { removeWatermark } from '../components/Watermark';
|
||||
import {
|
||||
removeEmailFunctionality,
|
||||
isEmailFunctionalitySetup,
|
||||
} from '../components/EmailButton';
|
||||
import { restoreOriginalFields } from '../helpers/fieldHelpers';
|
||||
import useCustomerData from './useCustomerData';
|
||||
|
||||
/**
|
||||
* Custom hook to handle cleanup of AXO functionality.
|
||||
* This hook ensures that all AXO-related changes are reverted when the component unmounts (a different payment method gets selected).
|
||||
*/
|
||||
const useAxoCleanup = () => {
|
||||
// Get dispatch functions from the AXO store
|
||||
const { setIsAxoActive, setIsGuest, setIsEmailLookupCompleted } =
|
||||
useDispatch( STORE_NAME );
|
||||
|
||||
// Get functions to update WooCommerce shipping and billing addresses
|
||||
const {
|
||||
setShippingAddress: updateWooShippingAddress,
|
||||
setBillingAddress: updateWooBillingAddress,
|
||||
} = useCustomerData();
|
||||
|
||||
// Effect to restore original WooCommerce fields on unmount
|
||||
useEffect( () => {
|
||||
return () => {
|
||||
log( 'Cleaning up: Restoring WooCommerce fields' );
|
||||
restoreOriginalFields(
|
||||
updateWooShippingAddress,
|
||||
updateWooBillingAddress
|
||||
);
|
||||
};
|
||||
}, [ updateWooShippingAddress, updateWooBillingAddress ] );
|
||||
|
||||
// Effect to clean up AXO-specific functionality on unmount
|
||||
useEffect( () => {
|
||||
return () => {
|
||||
log( 'Cleaning up Axo component' );
|
||||
|
||||
// Reset AXO state
|
||||
setIsAxoActive( false );
|
||||
setIsGuest( true );
|
||||
setIsEmailLookupCompleted( false );
|
||||
|
||||
// Remove AXO UI elements
|
||||
removeShippingChangeButton();
|
||||
removeCardChangeButton();
|
||||
removeWatermark();
|
||||
|
||||
// Remove email functionality if it was set up
|
||||
if ( isEmailFunctionalitySetup() ) {
|
||||
log( 'Removing email functionality' );
|
||||
removeEmailFunctionality();
|
||||
}
|
||||
};
|
||||
}, [] );
|
||||
};
|
||||
|
||||
export default useAxoCleanup;
|
111
modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js
Normal file
111
modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { useEffect } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
import usePayPalScript from './usePayPalScript';
|
||||
import { setupWatermark } from '../components/Watermark';
|
||||
import { setupEmailFunctionality } from '../components/EmailButton';
|
||||
import { createEmailLookupHandler } from '../events/emailLookupManager';
|
||||
import usePhoneSyncHandler from './usePhoneSyncHandler';
|
||||
import { initializeClassToggles } from '../helpers/classnamesManager';
|
||||
import { snapshotFields } from '../helpers/fieldHelpers';
|
||||
import useCustomerData from './useCustomerData';
|
||||
import useShippingAddressChange from './useShippingAddressChange';
|
||||
import useCardChange from './useCardChange';
|
||||
|
||||
/**
|
||||
* Custom hook to set up AXO functionality.
|
||||
*
|
||||
* @param {string} namespace - Namespace for the PayPal script.
|
||||
* @param {Object} ppcpConfig - PayPal Checkout configuration.
|
||||
* @param {boolean} isConfigLoaded - Whether the PayPal config has loaded.
|
||||
* @param {Object} fastlaneSdk - Fastlane SDK instance.
|
||||
* @param {Object} paymentComponent - Payment component instance.
|
||||
* @return {boolean} Whether PayPal script has loaded.
|
||||
*/
|
||||
const useAxoSetup = (
|
||||
namespace,
|
||||
ppcpConfig,
|
||||
isConfigLoaded,
|
||||
fastlaneSdk,
|
||||
paymentComponent
|
||||
) => {
|
||||
// Get dispatch functions from the AXO store
|
||||
const {
|
||||
setIsAxoActive,
|
||||
setIsAxoScriptLoaded,
|
||||
setShippingAddress,
|
||||
setCardDetails,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
||||
// Check if PayPal script has loaded
|
||||
const paypalLoaded = usePayPalScript(
|
||||
namespace,
|
||||
ppcpConfig,
|
||||
isConfigLoaded
|
||||
);
|
||||
|
||||
// Set up card and shipping address change handlers
|
||||
const onChangeCardButtonClick = useCardChange( fastlaneSdk );
|
||||
const onChangeShippingAddressClick = useShippingAddressChange(
|
||||
fastlaneSdk,
|
||||
setShippingAddress
|
||||
);
|
||||
|
||||
// Get customer data and setter functions
|
||||
const {
|
||||
shippingAddress: wooShippingAddress,
|
||||
billingAddress: wooBillingAddress,
|
||||
setShippingAddress: setWooShippingAddress,
|
||||
setBillingAddress: setWooBillingAddress,
|
||||
} = useCustomerData();
|
||||
|
||||
// Set up phone sync handler
|
||||
usePhoneSyncHandler( paymentComponent );
|
||||
|
||||
// Initialize class toggles on mount
|
||||
useEffect( () => {
|
||||
initializeClassToggles();
|
||||
}, [] );
|
||||
|
||||
// Set up AXO functionality when PayPal and Fastlane are loaded
|
||||
useEffect( () => {
|
||||
setupWatermark( fastlaneSdk );
|
||||
if ( paypalLoaded && fastlaneSdk ) {
|
||||
setIsAxoScriptLoaded( true );
|
||||
setIsAxoActive( true );
|
||||
|
||||
// Create and set up email lookup handler
|
||||
const emailLookupHandler = createEmailLookupHandler(
|
||||
fastlaneSdk,
|
||||
setShippingAddress,
|
||||
setCardDetails,
|
||||
snapshotFields,
|
||||
wooShippingAddress,
|
||||
wooBillingAddress,
|
||||
setWooShippingAddress,
|
||||
setWooBillingAddress,
|
||||
onChangeShippingAddressClick,
|
||||
onChangeCardButtonClick
|
||||
);
|
||||
setupEmailFunctionality( emailLookupHandler );
|
||||
}
|
||||
}, [
|
||||
paypalLoaded,
|
||||
fastlaneSdk,
|
||||
setIsAxoActive,
|
||||
setIsAxoScriptLoaded,
|
||||
wooShippingAddress,
|
||||
wooBillingAddress,
|
||||
setWooShippingAddress,
|
||||
setWooBillingAddress,
|
||||
onChangeShippingAddressClick,
|
||||
onChangeCardButtonClick,
|
||||
setShippingAddress,
|
||||
setCardDetails,
|
||||
paymentComponent,
|
||||
] );
|
||||
|
||||
return paypalLoaded;
|
||||
};
|
||||
|
||||
export default useAxoSetup;
|
82
modules/ppcp-axo-block/resources/js/hooks/useCardChange.js
Normal file
82
modules/ppcp-axo-block/resources/js/hooks/useCardChange.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { useCallback } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { useAddressEditing } from './useAddressEditing';
|
||||
import useCustomerData from './useCustomerData';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
|
||||
/**
|
||||
* Custom hook to handle the 'Choose a different card' selection.
|
||||
*
|
||||
* @param {Object} fastlaneSdk - The Fastlane SDK instance.
|
||||
* @return {Function} Callback function to trigger card selection and update related data.
|
||||
*/
|
||||
export const useCardChange = ( fastlaneSdk ) => {
|
||||
const { setBillingAddressEditing } = useAddressEditing();
|
||||
const { setBillingAddress: setWooBillingAddress } = useCustomerData();
|
||||
const { setCardDetails } = useDispatch( STORE_NAME );
|
||||
|
||||
return useCallback( async () => {
|
||||
if ( fastlaneSdk ) {
|
||||
// Show card selector and get the user's selection
|
||||
const { selectionChanged, selectedCard } =
|
||||
await fastlaneSdk.profile.showCardSelector();
|
||||
|
||||
if ( selectionChanged && selectedCard?.paymentSource?.card ) {
|
||||
// Extract cardholder and billing information from the selected card
|
||||
const { name, billingAddress } =
|
||||
selectedCard.paymentSource.card;
|
||||
|
||||
// Parse cardholder's name, using billing details as a fallback if missing
|
||||
let firstName = '';
|
||||
let lastName = '';
|
||||
|
||||
if ( name ) {
|
||||
const nameParts = name.split( ' ' );
|
||||
firstName = nameParts[ 0 ];
|
||||
lastName = nameParts.slice( 1 ).join( ' ' );
|
||||
}
|
||||
|
||||
// Transform the billing address into WooCommerce format
|
||||
const newBillingAddress = {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
address_1: billingAddress?.addressLine1 || '',
|
||||
address_2: billingAddress?.addressLine2 || '',
|
||||
city: billingAddress?.adminArea2 || '',
|
||||
state: billingAddress?.adminArea1 || '',
|
||||
postcode: billingAddress?.postalCode || '',
|
||||
country: billingAddress?.countryCode || '',
|
||||
};
|
||||
|
||||
// Batch update states
|
||||
await Promise.all( [
|
||||
// Update the selected card details in the custom store
|
||||
new Promise( ( resolve ) => {
|
||||
setCardDetails( selectedCard );
|
||||
resolve();
|
||||
} ),
|
||||
// Update the WooCommerce billing address in the WooCommerce store
|
||||
new Promise( ( resolve ) => {
|
||||
setWooBillingAddress( newBillingAddress );
|
||||
resolve();
|
||||
} ),
|
||||
// Trigger the Address Card view by setting the billing address editing state to false
|
||||
new Promise( ( resolve ) => {
|
||||
setBillingAddressEditing( false );
|
||||
resolve();
|
||||
} ),
|
||||
] );
|
||||
} else {
|
||||
log( 'Selected card or billing address is missing.', 'error' );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
fastlaneSdk,
|
||||
setCardDetails,
|
||||
setWooBillingAddress,
|
||||
setBillingAddressEditing,
|
||||
] );
|
||||
};
|
||||
|
||||
export default useCardChange;
|
54
modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js
Normal file
54
modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { useCallback, useMemo } from '@wordpress/element';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Custom hook to manage customer data in the WooCommerce store.
|
||||
*
|
||||
* @return {Object} An object containing customer addresses and setter functions.
|
||||
*/
|
||||
export const useCustomerData = () => {
|
||||
// Fetch customer data from the WooCommerce store
|
||||
const customerData = useSelect( ( select ) =>
|
||||
select( 'wc/store/cart' ).getCustomerData()
|
||||
);
|
||||
|
||||
// Get dispatch functions to update shipping and billing addresses
|
||||
const {
|
||||
setShippingAddress: setShippingAddressDispatch,
|
||||
setBillingAddress: setBillingAddressDispatch,
|
||||
} = useDispatch( 'wc/store/cart' );
|
||||
|
||||
// Memoized function to update shipping address
|
||||
const setShippingAddress = useCallback(
|
||||
( address ) => {
|
||||
setShippingAddressDispatch( address );
|
||||
},
|
||||
[ setShippingAddressDispatch ]
|
||||
);
|
||||
|
||||
// Memoized function to update billing address
|
||||
const setBillingAddress = useCallback(
|
||||
( address ) => {
|
||||
setBillingAddressDispatch( address );
|
||||
},
|
||||
[ setBillingAddressDispatch ]
|
||||
);
|
||||
|
||||
// Return memoized object with customer data and setter functions
|
||||
return useMemo(
|
||||
() => ( {
|
||||
shippingAddress: customerData.shippingAddress,
|
||||
billingAddress: customerData.billingAddress,
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
} ),
|
||||
[
|
||||
customerData.shippingAddress,
|
||||
customerData.billingAddress,
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
export default useCustomerData;
|
|
@ -0,0 +1,45 @@
|
|||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
const isObject = ( value ) => typeof value === 'object' && value !== null;
|
||||
const isNonEmptyString = ( value ) => value !== '';
|
||||
|
||||
/**
|
||||
* Recursively removes empty values from an object.
|
||||
* Empty values are considered to be:
|
||||
* - Empty strings
|
||||
* - Empty objects
|
||||
* - Null or undefined values
|
||||
*
|
||||
* @param {Object} obj - The object to clean.
|
||||
* @return {Object} A new object with empty values removed.
|
||||
*/
|
||||
const removeEmptyValues = ( obj ) => {
|
||||
// If not an object, return the value as is
|
||||
if ( ! isObject( obj ) ) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries( obj )
|
||||
// Recursively apply removeEmptyValues to nested objects
|
||||
.map( ( [ key, value ] ) => [
|
||||
key,
|
||||
isObject( value ) ? removeEmptyValues( value ) : value,
|
||||
] )
|
||||
// Filter out empty values
|
||||
.filter( ( [ _, value ] ) =>
|
||||
isObject( value )
|
||||
? Object.keys( value ).length > 0
|
||||
: isNonEmptyString( value )
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook that returns a memoized function to remove empty values from an object.
|
||||
*
|
||||
* @return {Function} A memoized function that removes empty values from an object.
|
||||
*/
|
||||
export const useDeleteEmptyKeys = () => {
|
||||
return useCallback( removeEmptyValues, [] );
|
||||
};
|
91
modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js
Normal file
91
modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
import { useEffect, useRef, useState, useMemo } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { useDeleteEmptyKeys } from './useDeleteEmptyKeys';
|
||||
import useAllowedLocations from './useAllowedLocations';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
|
||||
/**
|
||||
* Custom hook to initialize and manage the Fastlane SDK.
|
||||
*
|
||||
* @param {string} namespace - Namespace for the PayPal script.
|
||||
* @param {Object} axoConfig - Configuration for AXO.
|
||||
* @param {Object} ppcpConfig - Configuration for PPCP.
|
||||
* @return {Object|null} The initialized Fastlane SDK instance or null.
|
||||
*/
|
||||
const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
|
||||
const [ fastlaneSdk, setFastlaneSdk ] = useState( null );
|
||||
const initializingRef = useRef( false );
|
||||
const configRef = useRef( { axoConfig, ppcpConfig } );
|
||||
const deleteEmptyKeys = useDeleteEmptyKeys();
|
||||
|
||||
const { isPayPalLoaded } = useSelect(
|
||||
( select ) => ( {
|
||||
isPayPalLoaded: select( STORE_NAME ).getIsPayPalLoaded(),
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
const styleOptions = useMemo( () => {
|
||||
return deleteEmptyKeys( configRef.current.axoConfig.style_options );
|
||||
}, [ deleteEmptyKeys ] );
|
||||
|
||||
const allowedLocations = useAllowedLocations( axoConfig );
|
||||
|
||||
// Effect to initialize Fastlane SDK
|
||||
useEffect( () => {
|
||||
const initFastlane = async () => {
|
||||
if ( initializingRef.current || fastlaneSdk || ! isPayPalLoaded ) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializingRef.current = true;
|
||||
log( 'Init Fastlane' );
|
||||
|
||||
try {
|
||||
const fastlane = new Fastlane( namespace );
|
||||
|
||||
// Set sandbox environment if configured
|
||||
if ( configRef.current.axoConfig.environment.is_sandbox ) {
|
||||
window.localStorage.setItem( 'axoEnv', 'sandbox' );
|
||||
}
|
||||
|
||||
// Connect to Fastlane with locale and style options
|
||||
await fastlane.connect( {
|
||||
locale: configRef.current.ppcpConfig.locale,
|
||||
styles: styleOptions,
|
||||
shippingAddressOptions: {
|
||||
allowedLocations,
|
||||
},
|
||||
} );
|
||||
|
||||
// Set locale (hardcoded to 'en_us' for now)
|
||||
fastlane.setLocale( 'en_us' );
|
||||
|
||||
setFastlaneSdk( fastlane );
|
||||
} catch ( error ) {
|
||||
log( `Failed to initialize Fastlane: ${ error }`, 'error' );
|
||||
} finally {
|
||||
initializingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
initFastlane();
|
||||
}, [
|
||||
fastlaneSdk,
|
||||
styleOptions,
|
||||
isPayPalLoaded,
|
||||
namespace,
|
||||
allowedLocations,
|
||||
] );
|
||||
|
||||
// Effect to update the config ref when configs change
|
||||
useEffect( () => {
|
||||
configRef.current = { axoConfig, ppcpConfig };
|
||||
}, [ axoConfig, ppcpConfig ] );
|
||||
|
||||
return fastlaneSdk;
|
||||
};
|
||||
|
||||
export default useFastlaneSdk;
|
|
@ -0,0 +1,70 @@
|
|||
import { useCallback } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
|
||||
/**
|
||||
* Custom hook to handle payment setup in the checkout process.
|
||||
*
|
||||
* @param {Object} emitResponse - Object containing response types.
|
||||
* @param {Object} paymentComponent - The payment component instance.
|
||||
* @param {Object} tokenizedCustomerData - Tokenized customer data for payment.
|
||||
* @return {Function} Callback function to handle payment setup.
|
||||
*/
|
||||
const useHandlePaymentSetup = (
|
||||
emitResponse,
|
||||
paymentComponent,
|
||||
tokenizedCustomerData
|
||||
) => {
|
||||
// Select card details from the store
|
||||
const { cardDetails } = useSelect(
|
||||
( select ) => ( {
|
||||
cardDetails: select( STORE_NAME ).getCardDetails(),
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
return useCallback( async () => {
|
||||
// Determine if it's a Ryan flow (saved card) based on the presence of card ID
|
||||
const isRyanFlow = !! cardDetails?.id;
|
||||
let cardToken = cardDetails?.id;
|
||||
|
||||
// If no card token and payment component exists, get a new token
|
||||
if ( ! cardToken && paymentComponent ) {
|
||||
cardToken = await paymentComponent
|
||||
.getPaymentToken( tokenizedCustomerData )
|
||||
.then( ( response ) => response.id );
|
||||
}
|
||||
|
||||
// Handle error cases when card token is not available
|
||||
if ( ! cardToken ) {
|
||||
let reason = 'tokenization error';
|
||||
|
||||
if ( ! paymentComponent ) {
|
||||
reason = 'initialization error';
|
||||
}
|
||||
|
||||
return {
|
||||
type: emitResponse.responseTypes.ERROR,
|
||||
message: `Could not process the payment (${ reason })`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: emitResponse.responseTypes.SUCCESS,
|
||||
meta: {
|
||||
paymentMethodData: {
|
||||
fastlane_member: isRyanFlow,
|
||||
axo_nonce: cardToken,
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [
|
||||
cardDetails?.id,
|
||||
emitResponse.responseTypes.ERROR,
|
||||
emitResponse.responseTypes.SUCCESS,
|
||||
paymentComponent,
|
||||
tokenizedCustomerData,
|
||||
] );
|
||||
};
|
||||
|
||||
export default useHandlePaymentSetup;
|
|
@ -0,0 +1,46 @@
|
|||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
|
||||
/**
|
||||
* Custom hook to load and manage the PayPal Commerce Gateway configuration.
|
||||
*
|
||||
* @param {Object} initialConfig - Initial configuration object.
|
||||
* @return {Object} An object containing the loaded config and a boolean indicating if it's loaded.
|
||||
*/
|
||||
const usePayPalCommerceGateway = ( initialConfig ) => {
|
||||
const [ isConfigLoaded, setIsConfigLoaded ] = useState( false );
|
||||
const [ ppcpConfig, setPpcpConfig ] = useState( initialConfig );
|
||||
|
||||
useEffect( () => {
|
||||
/**
|
||||
* Function to load the PayPal Commerce Gateway configuration.
|
||||
*/
|
||||
const loadConfig = () => {
|
||||
if ( typeof window.PayPalCommerceGateway !== 'undefined' ) {
|
||||
setPpcpConfig( window.PayPalCommerceGateway );
|
||||
setIsConfigLoaded( true );
|
||||
} else {
|
||||
log( 'PayPal Commerce Gateway config not loaded.', 'error' );
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the DOM is still loading
|
||||
if ( document.readyState === 'loading' ) {
|
||||
// If it's loading, add an event listener for when the DOM is fully loaded
|
||||
document.addEventListener( 'DOMContentLoaded', loadConfig );
|
||||
} else {
|
||||
// If it's already loaded, call the loadConfig function immediately
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
// Cleanup function to remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener( 'DOMContentLoaded', loadConfig );
|
||||
};
|
||||
}, [] );
|
||||
|
||||
// Return the loaded configuration and the loading status
|
||||
return { isConfigLoaded, ppcpConfig };
|
||||
};
|
||||
|
||||
export default usePayPalCommerceGateway;
|
48
modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js
Normal file
48
modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { useEffect } from '@wordpress/element';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { loadPayPalScript } from '../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
|
||||
/**
|
||||
* Custom hook to load the PayPal script.
|
||||
*
|
||||
* @param {string} namespace - Namespace for the PayPal script.
|
||||
* @param {Object} ppcpConfig - Configuration object for PayPal script.
|
||||
* @param {boolean} isConfigLoaded - Whether the PayPal Commerce Gateway config is loaded.
|
||||
* @return {boolean} True if the PayPal script has loaded, false otherwise.
|
||||
*/
|
||||
const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => {
|
||||
// Get dispatch functions from the AXO store
|
||||
const { setIsPayPalLoaded } = useDispatch( STORE_NAME );
|
||||
|
||||
// Select relevant states from the AXO store
|
||||
const { isPayPalLoaded } = useSelect(
|
||||
( select ) => ( {
|
||||
isPayPalLoaded: select( STORE_NAME ).getIsPayPalLoaded(),
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
const loadScript = async () => {
|
||||
if ( ! isPayPalLoaded && isConfigLoaded ) {
|
||||
try {
|
||||
await loadPayPalScript( namespace, ppcpConfig );
|
||||
setIsPayPalLoaded( true );
|
||||
} catch ( error ) {
|
||||
log(
|
||||
`Error loading PayPal script for namespace: ${ namespace }. Error: ${ error }`,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadScript();
|
||||
}, [ ppcpConfig, isConfigLoaded, isPayPalLoaded ] );
|
||||
|
||||
return isPayPalLoaded;
|
||||
};
|
||||
|
||||
export default usePayPalScript;
|
|
@ -0,0 +1,43 @@
|
|||
import { useEffect, useCallback } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Custom hook to handle payment setup effects in the checkout flow.
|
||||
*
|
||||
* @param {Function} onPaymentSetup - Function to subscribe to payment setup events.
|
||||
* @param {Function} handlePaymentSetup - Callback to process payment setup.
|
||||
* @param {Function} setPaymentComponent - Function to update the payment component state.
|
||||
* @return {Object} Object containing the handlePaymentLoad function.
|
||||
*/
|
||||
const usePaymentSetupEffect = (
|
||||
onPaymentSetup,
|
||||
handlePaymentSetup,
|
||||
setPaymentComponent
|
||||
) => {
|
||||
/**
|
||||
* `onPaymentSetup()` fires when we enter the "PROCESSING" state in the checkout flow.
|
||||
* It pre-processes the payment details and returns data for server-side processing.
|
||||
*/
|
||||
useEffect( () => {
|
||||
const unsubscribe = onPaymentSetup( handlePaymentSetup );
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [ onPaymentSetup, handlePaymentSetup ] );
|
||||
|
||||
/**
|
||||
* Callback function to handle payment component loading.
|
||||
*
|
||||
* @param {Object} component - The loaded payment component.
|
||||
*/
|
||||
const handlePaymentLoad = useCallback(
|
||||
( component ) => {
|
||||
setPaymentComponent( component );
|
||||
},
|
||||
[ setPaymentComponent ]
|
||||
);
|
||||
|
||||
return { handlePaymentLoad };
|
||||
};
|
||||
|
||||
export default usePaymentSetupEffect;
|
|
@ -0,0 +1,88 @@
|
|||
import { useEffect, useRef, useCallback } from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
|
||||
import { debounce } from '../../../../ppcp-blocks/resources/js/Helper/debounce';
|
||||
import { STORE_NAME } from '../stores/axoStore';
|
||||
import useCustomerData from './useCustomerData';
|
||||
|
||||
const PHONE_DEBOUNCE_DELAY = 250;
|
||||
|
||||
/**
|
||||
* Sanitizes a phone number by removing country code and non-numeric characters.
|
||||
* Only returns the sanitized number if it's exactly 10 digits long (US phone number).
|
||||
*
|
||||
* @param {string} phoneNumber - The phone number to sanitize.
|
||||
* @return {string} The sanitized phone number; an empty string if it's invalid.
|
||||
*/
|
||||
const sanitizePhoneNumber = ( phoneNumber = '' ) => {
|
||||
const localNumber = phoneNumber.replace( /^\+?[01]+/, '' );
|
||||
const cleanNumber = localNumber.replace( /[^0-9]/g, '' );
|
||||
return cleanNumber.length === 10 ? cleanNumber : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the prefilled phone number in the Fastlane CardField component.
|
||||
*
|
||||
* @param {Object} paymentComponent - The CardField component from Fastlane
|
||||
* @param {string} phoneNumber - The new phone number to prefill.
|
||||
*/
|
||||
const updatePrefills = ( paymentComponent, phoneNumber ) => {
|
||||
log( `Update the phone prefill value: ${ phoneNumber }` );
|
||||
paymentComponent.updatePrefills( { phoneNumber } );
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to synchronize the WooCommerce phone number with a React component state.
|
||||
*
|
||||
* @param {Object} paymentComponent - The CardField component from Fastlane.
|
||||
*/
|
||||
const usePhoneSyncHandler = ( paymentComponent ) => {
|
||||
const { setPhoneNumber } = useDispatch( STORE_NAME );
|
||||
|
||||
const { phoneNumber } = useSelect( ( select ) => ( {
|
||||
phoneNumber: select( STORE_NAME ).getPhoneNumber(),
|
||||
} ) );
|
||||
|
||||
const { shippingAddress, billingAddress } = useCustomerData();
|
||||
|
||||
// Create a debounced function that updates the prefilled phone-number.
|
||||
const debouncedUpdatePhone = useRef(
|
||||
debounce( updatePrefills, PHONE_DEBOUNCE_DELAY )
|
||||
).current;
|
||||
|
||||
// Fetch and update the phone number from the billing or shipping address.
|
||||
const fetchAndUpdatePhoneNumber = useCallback( () => {
|
||||
const billingPhone = billingAddress?.phone || '';
|
||||
const shippingPhone = shippingAddress?.phone || '';
|
||||
const sanitizedPhoneNumber = sanitizePhoneNumber(
|
||||
billingPhone || shippingPhone
|
||||
);
|
||||
|
||||
if ( sanitizedPhoneNumber && sanitizedPhoneNumber !== phoneNumber ) {
|
||||
setPhoneNumber( sanitizedPhoneNumber );
|
||||
}
|
||||
}, [ billingAddress, shippingAddress, phoneNumber, setPhoneNumber ] );
|
||||
|
||||
// Fetch and update the phone number from the billing or shipping address.
|
||||
useEffect( () => {
|
||||
fetchAndUpdatePhoneNumber();
|
||||
}, [ fetchAndUpdatePhoneNumber ] );
|
||||
|
||||
// Invoke debounced function when paymentComponent or phoneNumber changes.
|
||||
useEffect( () => {
|
||||
if ( paymentComponent && phoneNumber ) {
|
||||
debouncedUpdatePhone( paymentComponent, phoneNumber );
|
||||
}
|
||||
}, [ debouncedUpdatePhone, paymentComponent, phoneNumber ] );
|
||||
|
||||
// Cleanup on unmount, canceling any pending debounced calls.
|
||||
useEffect( () => {
|
||||
return () => {
|
||||
if ( debouncedUpdatePhone?.cancel ) {
|
||||
debouncedUpdatePhone.cancel();
|
||||
}
|
||||
};
|
||||
}, [ debouncedUpdatePhone ] );
|
||||
};
|
||||
|
||||
export default usePhoneSyncHandler;
|
|
@ -0,0 +1,62 @@
|
|||
import { useCallback } from '@wordpress/element';
|
||||
import { useAddressEditing } from './useAddressEditing';
|
||||
import useCustomerData from './useCustomerData';
|
||||
|
||||
/**
|
||||
* Custom hook to handle the 'Choose a different shipping address' selection.
|
||||
*
|
||||
* @param {Object} fastlaneSdk - The Fastlane SDK instance.
|
||||
* @param {Function} setShippingAddress - Function to update the shipping address state.
|
||||
* @return {Function} Callback function to trigger shipping address selection and update.
|
||||
*/
|
||||
export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => {
|
||||
const { setShippingAddressEditing } = useAddressEditing();
|
||||
const { setShippingAddress: setWooShippingAddress } = useCustomerData();
|
||||
|
||||
return useCallback( async () => {
|
||||
if ( fastlaneSdk ) {
|
||||
// Show shipping address selector and get the user's selection
|
||||
const { selectionChanged, selectedAddress } =
|
||||
await fastlaneSdk.profile.showShippingAddressSelector();
|
||||
|
||||
if ( selectionChanged ) {
|
||||
// Update the shipping address in the custom store with the selected address
|
||||
setShippingAddress( selectedAddress );
|
||||
|
||||
const { address, name, phoneNumber } = selectedAddress;
|
||||
|
||||
// Transform the selected address into WooCommerce format
|
||||
const newShippingAddress = {
|
||||
first_name: name.firstName,
|
||||
last_name: name.lastName,
|
||||
address_1: address.addressLine1,
|
||||
address_2: address.addressLine2 || '',
|
||||
city: address.adminArea2,
|
||||
state: address.adminArea1,
|
||||
postcode: address.postalCode,
|
||||
country: address.countryCode,
|
||||
phone: phoneNumber.nationalNumber,
|
||||
};
|
||||
|
||||
// Update the WooCommerce shipping address in the WooCommerce store
|
||||
await new Promise( ( resolve ) => {
|
||||
setWooShippingAddress( newShippingAddress );
|
||||
resolve();
|
||||
} );
|
||||
|
||||
// Trigger the Address Card view by setting the shipping address editing state to false
|
||||
await new Promise( ( resolve ) => {
|
||||
setShippingAddressEditing( false );
|
||||
resolve();
|
||||
} );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
fastlaneSdk,
|
||||
setShippingAddress,
|
||||
setWooShippingAddress,
|
||||
setShippingAddressEditing,
|
||||
] );
|
||||
};
|
||||
|
||||
export default useShippingAddressChange;
|
|
@ -0,0 +1,57 @@
|
|||
import { useMemo } from '@wordpress/element';
|
||||
import useCustomerData from './useCustomerData';
|
||||
|
||||
/**
|
||||
* Custom hook to prepare customer data for tokenization.
|
||||
*
|
||||
* @return {Object} Formatted customer data for tokenization.
|
||||
*/
|
||||
export const useTokenizeCustomerData = () => {
|
||||
const { billingAddress, shippingAddress } = useCustomerData();
|
||||
|
||||
/**
|
||||
* Validates if an address contains the minimum required data.
|
||||
*
|
||||
* @param {Object} address - The address object to validate.
|
||||
* @return {boolean} True if the address is valid, false otherwise.
|
||||
*/
|
||||
const isValidAddress = ( address ) => {
|
||||
// At least one name must be present
|
||||
if ( ! address.first_name && ! address.last_name ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Street, city, postcode, country are mandatory; state is optional
|
||||
return (
|
||||
address.address_1 &&
|
||||
address.city &&
|
||||
address.postcode &&
|
||||
address.country
|
||||
);
|
||||
};
|
||||
|
||||
// Memoize the customer data to avoid unnecessary re-renders (and potential infinite loops)
|
||||
return useMemo( () => {
|
||||
// Determine the main address, preferring billing address if valid
|
||||
const mainAddress = isValidAddress( billingAddress )
|
||||
? billingAddress
|
||||
: shippingAddress;
|
||||
|
||||
// Format the customer data for tokenization
|
||||
return {
|
||||
cardholderName: {
|
||||
fullName: `${ mainAddress.first_name } ${ mainAddress.last_name }`,
|
||||
},
|
||||
billingAddress: {
|
||||
addressLine1: mainAddress.address_1,
|
||||
addressLine2: mainAddress.address_2,
|
||||
adminArea1: mainAddress.state,
|
||||
adminArea2: mainAddress.city,
|
||||
postalCode: mainAddress.postcode,
|
||||
countryCode: mainAddress.country,
|
||||
},
|
||||
};
|
||||
}, [ billingAddress, shippingAddress ] );
|
||||
};
|
||||
|
||||
export default useTokenizeCustomerData;
|
108
modules/ppcp-axo-block/resources/js/index.js
Normal file
108
modules/ppcp-axo-block/resources/js/index.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
import { useState, createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
|
||||
|
||||
// Hooks
|
||||
import useFastlaneSdk from './hooks/useFastlaneSdk';
|
||||
import useTokenizeCustomerData from './hooks/useTokenizeCustomerData';
|
||||
import useAxoSetup from './hooks/useAxoSetup';
|
||||
import useAxoCleanup from './hooks/useAxoCleanup';
|
||||
import useHandlePaymentSetup from './hooks/useHandlePaymentSetup';
|
||||
import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
|
||||
import usePayPalCommerceGateway from './hooks/usePayPalCommerceGateway';
|
||||
|
||||
// Components
|
||||
import { Payment } from './components/Payment/Payment';
|
||||
|
||||
const gatewayHandle = 'ppcp-axo-gateway';
|
||||
const namespace = 'ppcpBlocksPaypalAxo';
|
||||
const initialConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` );
|
||||
const Axo = ( props ) => {
|
||||
const { eventRegistration, emitResponse } = props;
|
||||
const { onPaymentSetup } = eventRegistration;
|
||||
const [ paymentComponent, setPaymentComponent ] = useState( null );
|
||||
|
||||
const { isConfigLoaded, ppcpConfig } =
|
||||
usePayPalCommerceGateway( initialConfig );
|
||||
|
||||
const axoConfig = window.wc_ppcp_axo;
|
||||
|
||||
const fastlaneSdk = useFastlaneSdk( namespace, axoConfig, ppcpConfig );
|
||||
const tokenizedCustomerData = useTokenizeCustomerData();
|
||||
const handlePaymentSetup = useHandlePaymentSetup(
|
||||
emitResponse,
|
||||
paymentComponent,
|
||||
tokenizedCustomerData
|
||||
);
|
||||
|
||||
const isScriptLoaded = useAxoSetup(
|
||||
namespace,
|
||||
ppcpConfig,
|
||||
isConfigLoaded,
|
||||
fastlaneSdk,
|
||||
paymentComponent
|
||||
);
|
||||
|
||||
const { handlePaymentLoad } = usePaymentSetupEffect(
|
||||
onPaymentSetup,
|
||||
handlePaymentSetup,
|
||||
setPaymentComponent
|
||||
);
|
||||
|
||||
useAxoCleanup();
|
||||
|
||||
if ( ! isConfigLoaded ) {
|
||||
return (
|
||||
<>
|
||||
{ __(
|
||||
'Loading configuration…',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! isScriptLoaded ) {
|
||||
return (
|
||||
<>
|
||||
{ __(
|
||||
'Loading PayPal script…',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! fastlaneSdk ) {
|
||||
return (
|
||||
<>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) }</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Payment
|
||||
fastlaneSdk={ fastlaneSdk }
|
||||
onPaymentLoad={ handlePaymentLoad }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
registerPaymentMethod( {
|
||||
name: initialConfig.id,
|
||||
label: (
|
||||
<div
|
||||
id="ppcp-axo-block-radio-label"
|
||||
dangerouslySetInnerHTML={ { __html: initialConfig.title } }
|
||||
/>
|
||||
),
|
||||
content: <Axo />,
|
||||
edit: createElement( initialConfig.title ),
|
||||
ariaLabel: initialConfig.title,
|
||||
canMakePayment: () => true,
|
||||
supports: {
|
||||
showSavedCards: true,
|
||||
features: initialConfig.supports,
|
||||
},
|
||||
} );
|
||||
|
||||
export default Axo;
|
165
modules/ppcp-axo-block/resources/js/stores/axoStore.js
Normal file
165
modules/ppcp-axo-block/resources/js/stores/axoStore.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { createReduxStore, register, dispatch } from '@wordpress/data';
|
||||
|
||||
export const STORE_NAME = 'woocommerce-paypal-payments/axo-block';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isPayPalLoaded: false,
|
||||
isGuest: true,
|
||||
isAxoActive: false,
|
||||
isAxoScriptLoaded: false,
|
||||
isEmailSubmitted: false,
|
||||
isEmailLookupCompleted: false,
|
||||
shippingAddress: null,
|
||||
cardDetails: null,
|
||||
phoneNumber: '',
|
||||
};
|
||||
|
||||
// Action creators for updating the store state
|
||||
const actions = {
|
||||
setIsPayPalLoaded: ( isPayPalLoaded ) => ( {
|
||||
type: 'SET_IS_PAYPAL_LOADED',
|
||||
payload: isPayPalLoaded,
|
||||
} ),
|
||||
setIsGuest: ( isGuest ) => ( {
|
||||
type: 'SET_IS_GUEST',
|
||||
payload: isGuest,
|
||||
} ),
|
||||
setIsAxoActive: ( isAxoActive ) => ( {
|
||||
type: 'SET_IS_AXO_ACTIVE',
|
||||
payload: isAxoActive,
|
||||
} ),
|
||||
setIsAxoScriptLoaded: ( isAxoScriptLoaded ) => ( {
|
||||
type: 'SET_IS_AXO_SCRIPT_LOADED',
|
||||
payload: isAxoScriptLoaded,
|
||||
} ),
|
||||
setIsEmailSubmitted: ( isEmailSubmitted ) => ( {
|
||||
type: 'SET_IS_EMAIL_SUBMITTED',
|
||||
payload: isEmailSubmitted,
|
||||
} ),
|
||||
setIsEmailLookupCompleted: ( isEmailLookupCompleted ) => ( {
|
||||
type: 'SET_IS_EMAIL_LOOKUP_COMPLETED',
|
||||
payload: isEmailLookupCompleted,
|
||||
} ),
|
||||
setShippingAddress: ( shippingAddress ) => ( {
|
||||
type: 'SET_SHIPPING_ADDRESS',
|
||||
payload: shippingAddress,
|
||||
} ),
|
||||
setCardDetails: ( cardDetails ) => ( {
|
||||
type: 'SET_CARD_DETAILS',
|
||||
payload: cardDetails,
|
||||
} ),
|
||||
setPhoneNumber: ( phoneNumber ) => ( {
|
||||
type: 'SET_PHONE_NUMBER',
|
||||
payload: phoneNumber,
|
||||
} ),
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer function to handle state updates based on dispatched actions.
|
||||
*
|
||||
* @param {Object} state - Current state of the store.
|
||||
* @param {Object} action - Dispatched action object.
|
||||
* @return {Object} New state after applying the action.
|
||||
*/
|
||||
const reducer = ( state = DEFAULT_STATE, action ) => {
|
||||
switch ( action.type ) {
|
||||
case 'SET_IS_PAYPAL_LOADED':
|
||||
return { ...state, isPayPalLoaded: action.payload };
|
||||
case 'SET_IS_GUEST':
|
||||
return { ...state, isGuest: action.payload };
|
||||
case 'SET_IS_AXO_ACTIVE':
|
||||
return { ...state, isAxoActive: action.payload };
|
||||
case 'SET_IS_AXO_SCRIPT_LOADED':
|
||||
return { ...state, isAxoScriptLoaded: action.payload };
|
||||
case 'SET_IS_EMAIL_SUBMITTED':
|
||||
return { ...state, isEmailSubmitted: action.payload };
|
||||
case 'SET_IS_EMAIL_LOOKUP_COMPLETED':
|
||||
return { ...state, isEmailLookupCompleted: action.payload };
|
||||
case 'SET_SHIPPING_ADDRESS':
|
||||
return { ...state, shippingAddress: action.payload };
|
||||
case 'SET_CARD_DETAILS':
|
||||
return { ...state, cardDetails: action.payload };
|
||||
case 'SET_PHONE_NUMBER':
|
||||
return { ...state, phoneNumber: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Selector functions to retrieve specific pieces of state
|
||||
const selectors = {
|
||||
getIsPayPalLoaded: ( state ) => state.isPayPalLoaded,
|
||||
getIsGuest: ( state ) => state.isGuest,
|
||||
getIsAxoActive: ( state ) => state.isAxoActive,
|
||||
getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded,
|
||||
getIsEmailSubmitted: ( state ) => state.isEmailSubmitted,
|
||||
getIsEmailLookupCompleted: ( state ) => state.isEmailLookupCompleted,
|
||||
getShippingAddress: ( state ) => state.shippingAddress,
|
||||
getCardDetails: ( state ) => state.cardDetails,
|
||||
getPhoneNumber: ( state ) => state.phoneNumber,
|
||||
};
|
||||
|
||||
// Create and register the Redux store for the AXO block
|
||||
const store = createReduxStore( STORE_NAME, {
|
||||
reducer,
|
||||
actions,
|
||||
selectors,
|
||||
} );
|
||||
|
||||
register( store );
|
||||
|
||||
// Action dispatchers
|
||||
|
||||
/**
|
||||
* Action dispatcher to update the PayPal script load status in the store.
|
||||
*
|
||||
* @param {boolean} isPayPalLoaded - Whether the PayPal script has loaded.
|
||||
*/
|
||||
export const setIsPayPalLoaded = ( isPayPalLoaded ) => {
|
||||
dispatch( STORE_NAME ).setIsPayPalLoaded( isPayPalLoaded );
|
||||
};
|
||||
|
||||
/**
|
||||
* Action dispatcher to update the guest status in the store.
|
||||
*
|
||||
* @param {boolean} isGuest - Whether the user is a guest or not.
|
||||
*/
|
||||
export const setIsGuest = ( isGuest ) => {
|
||||
dispatch( STORE_NAME ).setIsGuest( isGuest );
|
||||
};
|
||||
|
||||
/**
|
||||
* Action dispatcher to update the email lookup completion status in the store.
|
||||
*
|
||||
* @param {boolean} isEmailLookupCompleted - Whether the email lookup is completed.
|
||||
*/
|
||||
export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => {
|
||||
dispatch( STORE_NAME ).setIsEmailLookupCompleted( isEmailLookupCompleted );
|
||||
};
|
||||
|
||||
/**
|
||||
* Action dispatcher to update the shipping address in the store.
|
||||
*
|
||||
* @param {Object} shippingAddress - The user's shipping address.
|
||||
*/
|
||||
export const setShippingAddress = ( shippingAddress ) => {
|
||||
dispatch( STORE_NAME ).setShippingAddress( shippingAddress );
|
||||
};
|
||||
|
||||
/**
|
||||
* Action dispatcher to update the card details in the store.
|
||||
*
|
||||
* @param {Object} cardDetails - The user's card details.
|
||||
*/
|
||||
export const setCardDetails = ( cardDetails ) => {
|
||||
dispatch( STORE_NAME ).setCardDetails( cardDetails );
|
||||
};
|
||||
|
||||
/**
|
||||
* Action dispatcher to update the phone number in the store.
|
||||
*
|
||||
* @param {string} phoneNumber - The user's phone number.
|
||||
*/
|
||||
export const setPhoneNumber = ( phoneNumber ) => {
|
||||
dispatch( STORE_NAME ).setPhoneNumber( phoneNumber );
|
||||
};
|
44
modules/ppcp-axo-block/services.php
Normal file
44
modules/ppcp-axo-block/services.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
/**
|
||||
* The Axo module services.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Axo
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\AxoBlock;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
|
||||
return array(
|
||||
// If AXO Block is configured and onboarded.
|
||||
'axoblock.available' => static function ( ContainerInterface $container ) : bool {
|
||||
return true;
|
||||
},
|
||||
'axoblock.url' => static function ( ContainerInterface $container ) : string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-axo-block/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
},
|
||||
'axoblock.method' => static function ( ContainerInterface $container ) : AxoBlockPaymentMethod {
|
||||
return new AxoBlockPaymentMethod(
|
||||
$container->get( 'axoblock.url' ),
|
||||
$container->get( 'ppcp.asset-version' ),
|
||||
$container->get( 'axo.gateway' ),
|
||||
fn() : SmartButtonInterface => $container->get( 'button.smart-button' ),
|
||||
$container->get( 'wcgateway.settings' ),
|
||||
$container->get( 'wcgateway.configuration.dcc' ),
|
||||
$container->get( 'onboarding.environment' ),
|
||||
$container->get( 'wcgateway.url' ),
|
||||
$container->get( 'axo.shipping-wc-enabled-locations' )
|
||||
);
|
||||
},
|
||||
);
|
169
modules/ppcp-axo-block/src/AxoBlockModule.php
Normal file
169
modules/ppcp-axo-block/src/AxoBlockModule.php
Normal file
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
/**
|
||||
* The Axo Block module.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\AxoBlock
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\AxoBlock;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
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 AxoBlockModule
|
||||
*/
|
||||
class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
||||
use ModuleClassNameIdTrait;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function services(): array {
|
||||
return require __DIR__ . '/../services.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
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' )
|
||||
) {
|
||||
add_action(
|
||||
'admin_notices',
|
||||
function () {
|
||||
printf(
|
||||
'<div class="notice notice-error"><p>%1$s</p></div>',
|
||||
wp_kses_post(
|
||||
__(
|
||||
'Fastlane checkout block initialization failed, possibly old WooCommerce version or disabled WooCommerce Blocks plugin.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_loaded',
|
||||
function () use ( $c ) {
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_localized_script_data',
|
||||
function( array $localized_script_data ) use ( $c ) {
|
||||
$module = $this;
|
||||
$api = $c->get( 'api.sdk-client-token' );
|
||||
assert( $api instanceof SdkClientToken );
|
||||
|
||||
$logger = $c->get( 'woocommerce.logger.woocommerce' );
|
||||
assert( $logger instanceof LoggerInterface );
|
||||
|
||||
return $module->add_sdk_client_token_to_script_data( $api, $logger, $localized_script_data );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Param types removed to avoid third-party issues.
|
||||
*
|
||||
* @psalm-suppress MissingClosureParamType
|
||||
*/
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_sdk_components_hook',
|
||||
function( $components ) {
|
||||
$components[] = 'fastlane';
|
||||
return $components;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_blocks_payment_method_type_registration',
|
||||
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
|
||||
/*
|
||||
* Only register the method if we are not in the admin
|
||||
* (to avoid two Debit & Credit Cards gateways in the
|
||||
* checkout block in the editor: one from ACDC one from Axo).
|
||||
*/
|
||||
if ( ! is_admin() ) {
|
||||
$payment_method_registry->register( $c->get( 'axoblock.method' ) );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Enqueue frontend scripts.
|
||||
add_action(
|
||||
'wp_enqueue_scripts',
|
||||
static function () use ( $c ) {
|
||||
if ( ! has_block( 'woocommerce/checkout' ) && ! has_block( 'woocommerce/cart' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$module_url = $c->get( 'axoblock.url' );
|
||||
$asset_version = $c->get( 'ppcp.asset-version' );
|
||||
|
||||
wp_register_style(
|
||||
'wc-ppcp-axo-block',
|
||||
untrailingslashit( $module_url ) . '/assets/css/gateway.css',
|
||||
array(),
|
||||
$asset_version
|
||||
);
|
||||
wp_enqueue_style( 'wc-ppcp-axo-block' );
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds id token to localized script data.
|
||||
*
|
||||
* @param SdkClientToken $api User id token api.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param array $localized_script_data The localized script data.
|
||||
* @return array
|
||||
*/
|
||||
private function add_sdk_client_token_to_script_data(
|
||||
SdkClientToken $api,
|
||||
LoggerInterface $logger,
|
||||
array $localized_script_data
|
||||
): array {
|
||||
try {
|
||||
$sdk_client_token = $api->sdk_client_token();
|
||||
$localized_script_data['axo'] = array(
|
||||
'sdk_client_token' => $sdk_client_token,
|
||||
);
|
||||
|
||||
} catch ( RuntimeException $exception ) {
|
||||
$error = $exception->getMessage();
|
||||
if ( is_a( $exception, PayPalApiException::class ) ) {
|
||||
$error = $exception->get_details( $error );
|
||||
}
|
||||
|
||||
$logger->error( $error );
|
||||
}
|
||||
|
||||
return $localized_script_data;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue