Merge branch 'trunk' into PCP-2722-add-block-checkout-compatibility-to-advanced-card-processing

This commit is contained in:
Emili Castells Guasch 2024-05-08 14:27:40 +02:00
commit 3d924e0a04
108 changed files with 10904 additions and 369 deletions

5
.gitignore vendored
View file

@ -4,9 +4,8 @@
node_modules
.phpunit.result.cache
yarn-error.log
modules/ppcp-button/assets/*
modules/ppcp-wc-gateway/assets/js
modules/ppcp-wc-gateway/assets/css
modules/*/vendor/*
modules/*/assets/*
*.zip
.env
.env.e2e

View file

@ -55,3 +55,13 @@ function as_unschedule_action($hook, $args = array(), $group = '') {}
* @return int The action ID.
*/
function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false ) {}
/**
* HTML API: WP_HTML_Tag_Processor class
*/
class WP_HTML_Tag_Processor {
public function __construct( $html ) {}
public function next_tag( $query = null ) {}
public function set_attribute( $name, $value ) {}
public function get_updated_html() {}
}

View file

@ -1,5 +1,20 @@
*** Changelog ***
= 2.7.0 - 2024-04-30 =
* Fix - Zero sum subscriptions cause CANNOT_BE_ZERO_OR_NEGATIVE when using Vault v3 #2152
* Fix - Incorrect Pricing Issue with Variable Subscriptions in PayPal Subscriptions Mode #2156
* Fix - Wrong return_url in multisite setup when using subdomains #2157
* Fix - Fix the fundingSource is not defined error on Block Checkout #2185
* Enhancement - Add the data-page-type attribute for JS SDK #2161
* Enhancement - Save Card Last Digits in order meta for Advanced Card Payments #2149
* Enhancement - Refactor the Pay Later Messaging block and add dedicated Cart/Checkout blocks #2153
* Enhancement - "Next Payment" status not updated when using PayPal Subscriptions #2091
* Enhancement - Optimize default settings for new store configurations #2158
* Enhancement - Improve tooltip information for tagline #2154
* Enhancement - Improve error message on certain exceptions #1354
* Enhancement - Cart Pay Later block: Change the default insert position #2179
* Enhancement - Messages Bootstrap: Add a render retry functionality #2181
= 2.6.1 - 2024-04-09 =
* Fix - Payment tokens fixes and adjustments #2106
* Fix - Pay upon Invoice: Add input validation to Experience Context fields #2092

View file

@ -6,6 +6,7 @@
*/
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule;
use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksModule;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\PayLaterConfiguratorModule;
use WooCommerce\PayPalCommerce\PluginModule;
@ -72,9 +73,20 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-paylater-block/module.php" )();
}
if ( PayLaterWCBlocksModule::is_module_loading_required() ) {
$modules[] = ( require "$modules_dir/ppcp-paylater-wc-blocks/module.php" )();
}
if ( PayLaterConfiguratorModule::is_enabled() ) {
$modules[] = ( require "$modules_dir/ppcp-paylater-configurator/module.php" )();
}
if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.axo_enabled',
getenv( 'PCP_AXO_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-axo/module.php" )();
}
return $modules;
};

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
@ -1633,4 +1634,11 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.sdk-client-token' => static function( ContainerInterface $container ): SdkClientToken {
return new SdkClientToken(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,109 @@
<?php
/**
* Generates user ID token for payer.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Authentication
*/
namespace WooCommerce\PayPalCommerce\ApiClient\Authentication;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WP_Error;
/**
* Class SdkClientToken
*/
class SdkClientToken {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* SdkClientToken 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;
}
/**
* Returns `sdk_client_token` which uniquely identifies the payer.
*
* @param string $target_customer_id Vaulted customer id.
*
* @return string
*
* @throws PayPalApiException If the request fails.
* @throws RuntimeException If something unexpected happens.
*/
public function sdk_client_token( string $target_customer_id = '' ): string {
$bearer = $this->bearer->bearer();
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$domain = wp_unslash( $_SERVER['HTTP_HOST'] ?? '' );
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=client_token&intent=sdk_init&domains[]=' . $domain;
if ( $target_customer_id ) {
$url = add_query_arg(
array(
'target_customer_id' => $target_customer_id,
),
$url
);
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/x-www-form-urlencoded',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException( $json, $status_code );
}
return $json->access_token;
}
}

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Exception;
use stdClass;
/**
* Class PayPalApiException
*/
@ -17,7 +19,7 @@ class PayPalApiException extends RuntimeException {
/**
* The JSON response object of PayPal.
*
* @var \stdClass
* @var stdClass
*/
private $response;
@ -31,10 +33,10 @@ class PayPalApiException extends RuntimeException {
/**
* PayPalApiException constructor.
*
* @param \stdClass|null $response The JSON object.
* @param int $status_code The HTTP status code.
* @param stdClass|null $response The JSON object.
* @param int $status_code The HTTP status code.
*/
public function __construct( \stdClass $response = null, int $status_code = 0 ) {
public function __construct( stdClass $response = null, int $status_code = 0 ) {
if ( is_null( $response ) ) {
$response = new \stdClass();
}
@ -65,7 +67,7 @@ class PayPalApiException extends RuntimeException {
*/
$this->response = $response;
$this->status_code = $status_code;
$message = $response->message;
$message = $this->get_customer_friendly_message( $response );
if ( $response->name ) {
$message = '[' . $response->name . '] ' . $message;
}
@ -141,4 +143,40 @@ class PayPalApiException extends RuntimeException {
return $details;
}
/**
* Returns a friendly message if the error detail is known.
*
* @param stdClass $json The response.
* @return string
*/
public function get_customer_friendly_message( stdClass $json ): string {
if ( empty( $json->details ) ) {
return $json->message;
}
$improved_keys_messages = array(
'PAYMENT_DENIED' => __( 'PayPal rejected the payment. Please reach out to the PayPal support for more information.', 'woocommerce-paypal-payments' ),
'TRANSACTION_REFUSED' => __( 'The transaction has been refused by the payment processor. Please reach out to the PayPal support for more information.', 'woocommerce-paypal-payments' ),
'DUPLICATE_INVOICE_ID' => __( 'The transaction has been refused because the Invoice ID already exists. Please create a new order or reach out to the store owner.', 'woocommerce-paypal-payments' ),
'PAYER_CANNOT_PAY' => __( 'There was a problem processing this transaction. Please reach out to the store owner.', 'woocommerce-paypal-payments' ),
'PAYEE_ACCOUNT_RESTRICTED' => __( 'There was a problem processing this transaction. Please reach out to the store owner.', 'woocommerce-paypal-payments' ),
'AGREEMENT_ALREADY_CANCELLED' => __( 'The requested agreement is already canceled. Please reach out to the PayPal support for more information.', 'woocommerce-paypal-payments' ),
);
$improved_errors = array_filter(
array_keys( $improved_keys_messages ),
function ( $key ) use ( $json ): bool {
foreach ( $json->details as $detail ) {
if ( isset( $detail->issue ) && $detail->issue === $key ) {
return true;
}
}
return false;
}
);
if ( $improved_errors ) {
$improved_errors = array_values( $improved_errors );
return $improved_keys_messages[ $improved_errors[0] ];
}
return $json->message;
}
}

View file

@ -54,7 +54,7 @@ class ApplicationContextRepository {
$payment_preference = $this->settings->has( 'payee_preferred' ) && $this->settings->get( 'payee_preferred' ) ?
ApplicationContext::PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED : ApplicationContext::PAYMENT_METHOD_UNRESTRICTED;
$context = new ApplicationContext(
network_home_url( \WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ),
home_url( \WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ),
(string) wc_get_checkout_url(),
(string) $brand_name,
$locale,

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -68,6 +68,8 @@ return array(
$device_eligibility_notes = __( 'The Apple Pay button will be visible both in previews and below the PayPal buttons in the shop.', 'woocommerce-paypal-payments' );
}
$module_url = $container->get( 'applepay.url' );
// Connection tab fields.
$fields = $insert_after(
$fields,
@ -91,10 +93,15 @@ return array(
$connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">';
return $insert_after(
$fields,
'allow_card_button_gateway',
'digital_wallet_heading',
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;" />',
$module_url,
__( 'Apple Pay', 'woocommerce-paypal-payments' )
),
'type' => 'checkbox',
'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => array( 'ppcp-disabled-checkbox' ),
@ -109,7 +116,7 @@ return array(
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
@ -122,6 +129,7 @@ return array(
)
),
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
)
);
@ -129,10 +137,25 @@ return array(
return $insert_after(
$fields,
'allow_card_button_gateway',
'digital_wallet_heading',
array(
'spacer' => array(
'title' => '',
'type' => 'ppcp-text',
'text' => '',
'class' => array(),
'classes' => array( 'ppcp-active-spacer' ),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'dcc',
'requirements' => 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;" />',
$module_url,
__( 'Apple Pay', 'woocommerce-paypal-payments' )
),
'type' => 'checkbox',
'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">'
@ -145,7 +168,7 @@ return array(
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
@ -160,10 +183,12 @@ return array(
->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' )
->action_visible( 'applepay_checkout_data_mode' )
->action_class( 'applepay_button_enabled', 'active' )
->to_array(),
)
),
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
'applepay_button_domain_registration' => array(
'title' => __( 'Domain Registration', 'woocommerce-paypal-payments' ),
@ -183,7 +208,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'applepay_button_domain_validation' => array(
@ -206,7 +231,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'applepay_button_device_eligibility' => array(
@ -224,7 +249,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'applepay_button_type' => array(
@ -241,7 +266,7 @@ return array(
'default' => 'pay',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'applepay_button_color' => array(
@ -259,7 +284,7 @@ return array(
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'applepay_button_language' => array(
@ -276,7 +301,7 @@ return array(
'default' => 'en',
'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'applepay_checkout_data_mode' => array(
@ -290,7 +315,7 @@ return array(
'default' => PropertiesDictionary::BILLING_DATA_MODE_DEFAULT,
'options' => PropertiesDictionary::billing_data_modes(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
)

View file

@ -39,7 +39,7 @@ class ApplepayButton {
this.log = function() {
if ( this.buttonConfig.is_debug ) {
console.log('[ApplePayButton]', ...arguments);
//console.log('[ApplePayButton]', ...arguments);
}
}

14
modules/ppcp-axo/.babelrc Normal file
View file

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

3
modules/ppcp-axo/.gitignore vendored Normal file
View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-axo",
"type": "dhii-mod",
"description": "Axo 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\\Axo\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,386 @@
<?php
/**
* The Axo module extensions.
*
* @package WooCommerce\PayPalCommerce\Axo
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo;
use WooCommerce\PayPalCommerce\Axo\Helper\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
$insert_after = function( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
$index = array_search( $key, $keys, true );
$pos = false === $index ? count( $array ) : $index + 1;
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
};
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
$module_url = $container->get( 'axo.url' );
// Standard Payments tab fields.
return $insert_after(
$fields,
'vault_enabled_dcc',
array(
'axo_heading' => array(
'heading' => __( 'Fastlane', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
'description' => wp_kses_post(
sprintf(
// translators: %1$s and %2$s is a link tag.
__(
'Offer an accelerated checkout experience that recognizes guest shoppers and autofills their details so they can pay in seconds.',
'woocommerce-paypal-payments'
),
'<a
rel="noreferrer noopener"
href="https://woo.com/document/woocommerce-paypal-payments/#vaulting-a-card"
>',
'</a>'
)
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'dcc', 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_enabled' => array(
'title' => __( 'Fastlane', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/fastlane.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Fastlane', 'woocommerce-paypal-payments' )
),
'type' => 'checkbox',
'label' => __( 'Enable Fastlane by PayPal', 'woocommerce-paypal-payments' )
. '<p class="description">'
. __( 'Help accelerate checkout for guests with PayPal\'s autofill solution.', 'woocommerce-paypal-payments' )
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => array( 'dcc', 'axo' ),
'requirements' => array( 'axo' ),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
array(
$display_manager
->rule()
->condition_element( 'axo_enabled', '1' )
->action_visible( 'axo_gateway_title' )
->action_visible( 'axo_privacy' )
->action_visible( 'axo_name_on_card' )
->action_visible( 'axo_style_heading' )
->action_class( 'axo_enabled', 'active' )
->to_array(),
$display_manager
->rule()
->condition_element( 'axo_enabled', '1' )
->condition_js_variable( 'ppcpAxoShowStyles', true )
->action_visible( 'axo_style_root_heading' )
->action_visible( 'axo_style_root_bg_color' )
->action_visible( 'axo_style_root_error_color' )
->action_visible( 'axo_style_root_font_family' )
->action_visible( 'axo_style_root_text_color_base' )
->action_visible( 'axo_style_root_font_size_base' )
->action_visible( 'axo_style_root_padding' )
->action_visible( 'axo_style_root_primary_color' )
->action_visible( 'axo_style_input_heading' )
->action_visible( 'axo_style_input_bg_color' )
->action_visible( 'axo_style_input_border_radius' )
->action_visible( 'axo_style_input_border_color' )
->action_visible( 'axo_style_input_border_width' )
->action_visible( 'axo_style_input_text_color_base' )
->action_visible( 'axo_style_input_focus_border_color' )
->to_array(),
)
),
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
'axo_gateway_title' => array(
'title' => __( 'Gateway Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'desc_tip' => true,
'description' => __(
'This controls the title of the Fastlane gateway the user sees on checkout.',
'woocommerce-paypal-payments'
),
'default' => __(
'Debit & Credit Cards',
'woocommerce-paypal-payments'
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_privacy' => array(
'title' => __( 'Privacy', 'woocommerce-paypal-payments' ),
'type' => 'select',
'label' => __(
'This setting will control whether Fastlane branding is shown by email field.
<p class="description">PayPal powers this accelerated checkout solution from Fastlane. Since you\'ll share consumers\' email addresses with PayPal, please consult your legal advisors on the apropriate privacy setting for your business.</p>',
'woocommerce-paypal-payments'
),
'desc_tip' => true,
'description' => __(
'This setting will control whether Fastlane branding is shown by email field.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'yes',
'options' => PropertiesDictionary::privacy_options(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => array( 'dcc', 'axo' ),
'requirements' => array( 'axo' ),
),
'axo_name_on_card' => array(
'title' => __( 'Display Name on Card', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'default' => 'yes',
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'label' => __( 'Enable this to display the "Name on Card" field for new Fastlane buyers.', 'woocommerce-paypal-payments' ),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => array(),
'requirements' => array( 'axo' ),
),
'axo_style_heading' => array(
'heading' => __( 'Advanced Style Settings (optional)', 'woocommerce-paypal-payments' ),
'heading_html' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__(
'Advanced Style Settings (optional) %1$sSee more%2$s %3$sSee less%4$s',
'woocommerce-paypal-payments'
),
'<a href="javascript:void(0)" id="ppcp-axo-style-more" onclick="jQuery(this).hide(); jQuery(\'#ppcp-axo-style-less\').show(); document.ppcpAxoShowStyles = true; jQuery(document).trigger(\'ppcp-display-change\');" style="font-weight: normal;">',
'</a>',
'<a href="javascript:void(0)" id="ppcp-axo-style-less" onclick="jQuery(this).hide(); jQuery(\'#ppcp-axo-style-more\').show(); document.ppcpAxoShowStyles = false; jQuery(document).trigger(\'ppcp-display-change\');" style="font-weight: normal; display: none;">',
'</a>'
),
'type' => 'ppcp-heading',
'description' => wp_kses_post(
sprintf(
// translators: %1$s and %2$s is a link tag.
__(
'Leave the default styling, or customize how Fastlane looks on your website. %1$sSee PayPal\'s developer docs%2$s for info',
'woocommerce-paypal-payments'
),
'<a href="https://www.paypal.com/us/fastlane" target="_blank">',
'</a>'
)
),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'dcc', 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_heading' => array(
'heading' => __( 'Root Settings', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
'description' => __(
'These apply to the overall Fastlane checkout module.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'screens' => array( State::STATE_ONBOARDED ),
'requirements' => array( 'dcc', 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_bg_color' => array(
'title' => __( 'Background Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#ffffff',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_primary_color' => array(
'title' => __( 'Primary Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#0057F',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_error_color' => array(
'title' => __( 'Error Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#D9360B',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_font_family' => array(
'title' => __( 'Font Family', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => 'PayPal-Open',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_text_color_base' => array(
'title' => __( 'Text Color Base', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#010B0D',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_font_size_base' => array(
'title' => __( 'Font Size Base', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '16px',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_root_padding' => array(
'title' => __( 'Padding', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '4px',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_heading' => array(
'heading' => __( 'Input Settings', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
'description' => __(
'These apply to the customer input fields on your Fastlane module.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'screens' => array( State::STATE_ONBOARDED ),
'requirements' => array( 'dcc', 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_bg_color' => array(
'title' => __( 'Background Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#ffffff',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_border_radius' => array(
'title' => __( 'Border Radius', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '0.25em',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_border_color' => array(
'title' => __( 'Border Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#DADDDD',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_border_width' => array(
'title' => __( 'Border Width', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '1px',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_text_color_base' => array(
'title' => __( 'Text Color Base', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#010B0D',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_style_input_focus_border_color' => array(
'title' => __( 'Focus Border Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'placeholder' => '#0057FF',
'classes' => array( 'ppcp-field-indent' ),
'default' => '',
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
)
);
},
);

View file

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

View file

@ -0,0 +1,34 @@
{
"name": "ppcp-axo",
"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": "^6.0.0",
"core-js": "^3.25.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,45 @@
.ppcp-axo-watermark-container {
max-width: 200px;
margin-top: 10px;
}
.ppcp-axo-payment-container {
padding: 1rem 0;
background-color: #ffffff;
&.hidden {
display: none;
}
}
.ppcp-axo-email-widget {
border: 1px solid #cccccc;
background-color: #eeeeee;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
text-align: center;
font-weight: bold;
color: #000000;
}
.ppcp-axo-field-hidden {
display: none;
}
.ppcp-axo-customer-details {
margin-bottom: 40px;
}
.axo-checkout-header-section {
display: flex;
align-items: baseline;
gap: 1em;
}
.ppcp-axo-order-button {
float: none;
width: 100%;
box-sizing: border-box;
margin: var(--global-md-spacing) 0 1em;
padding: 0.6em 1em;
}

View file

@ -0,0 +1,815 @@
import Fastlane from "./Connection/Fastlane";
import {log} from "./Helper/Debug";
import DomElementCollection from "./Components/DomElementCollection";
import ShippingView from "./Views/ShippingView";
import BillingView from "./Views/BillingView";
import CardView from "./Views/CardView";
import PayPalInsights from "./Insights/PayPalInsights";
import {disable,enable} from "../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler";
import {getCurrentPaymentMethod} from "../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState";
class AxoManager {
constructor(axoConfig, ppcpConfig) {
this.axoConfig = axoConfig;
this.ppcpConfig = ppcpConfig;
this.initialized = false;
this.fastlane = new Fastlane();
this.$ = jQuery;
this.hideGatewaySelection = false;
this.status = {
active: false,
validEmail: false,
hasProfile: false,
useEmailWidget: this.useEmailWidget()
};
this.data = {
email: null,
billing: null,
shipping: null,
card: null,
};
this.el = new DomElementCollection();
this.styles = {
root: {
backgroundColorPrimary: '#ffffff'
}
};
this.locale = 'en_us';
this.registerEventHandlers();
this.shippingView = new ShippingView(this.el.shippingAddressContainer.selector, this.el);
this.billingView = new BillingView(this.el.billingAddressContainer.selector, this.el);
this.cardView = new CardView(this.el.paymentContainer.selector + '-details', this.el, this);
document.axoDebugSetStatus = (key, value) => {
this.setStatus(key, value);
}
document.axoDebugObject = () => {
console.log(this);
return this;
}
if (
this.axoConfig?.insights?.enabled
&& this.axoConfig?.insights?.client_id
&& this.axoConfig?.insights?.session_id
) {
PayPalInsights.config(this.axoConfig?.insights?.client_id, { debug: true });
PayPalInsights.setSessionId(this.axoConfig?.insights?.session_id);
PayPalInsights.trackJsLoad();
if (document.querySelector('.woocommerce-checkout')) {
PayPalInsights.trackBeginCheckout({
amount: this.axoConfig?.insights?.amount,
page_type: 'checkout',
user_data: {
country: 'US',
is_store_member: false,
}
});
}
}
this.triggerGatewayChange();
}
registerEventHandlers() {
this.$(document).on('change', 'input[name=payment_method]', (ev) => {
const map = {
'ppcp-axo-gateway': 'card',
'ppcp-gateway': 'paypal',
}
PayPalInsights.trackSelectPaymentMethod({
payment_method_selected: map[ev.target.value] || 'other',
page_type: 'checkout'
});
});
// Listen to Gateway Radio button changes.
this.el.gatewayRadioButton.on('change', (ev) => {
if (ev.target.checked) {
this.activateAxo();
} else {
this.deactivateAxo();
}
});
this.$(document).on('updated_checkout payment_method_selected', () => {
this.triggerGatewayChange();
});
// On checkout form submitted.
this.el.submitButton.on('click', () => {
this.onClickSubmitButton();
return false;
})
// Click change shipping address link.
this.el.changeShippingAddressLink.on('click', async () => {
if (this.status.hasProfile) {
const { selectionChanged, selectedAddress } = await this.fastlane.profile.showShippingAddressSelector();
if (selectionChanged) {
this.setShipping(selectedAddress);
this.shippingView.refresh();
}
}
});
// Click change billing address link.
this.el.changeBillingAddressLink.on('click', async () => {
if (this.status.hasProfile) {
this.el.changeCardLink.trigger('click');
}
});
// Click change card link.
this.el.changeCardLink.on('click', async () => {
const response = await this.fastlane.profile.showCardSelector();
if (response.selectionChanged) {
this.setCard(response.selectedCard);
this.setBilling({
address: response.selectedCard.paymentSource.card.billingAddress
});
}
});
// Cancel "continuation" mode.
this.el.showGatewaySelectionLink.on('click', async () => {
this.hideGatewaySelection = false;
this.$('.wc_payment_methods label').show();
this.cardView.refresh();
});
// Prevents sending checkout form when pressing Enter key on input field
// and triggers customer lookup
this.$('form.woocommerce-checkout input').on('keydown', async (ev) => {
if(ev.key === 'Enter' && getCurrentPaymentMethod() === 'ppcp-axo-gateway' ) {
ev.preventDefault();
}
});
// Listening to status update event
document.addEventListener('axo_status_updated', (ev) => {
const termsField = document.querySelector("[name='terms-field']");
if(termsField) {
const status = ev.detail;
const shouldHide = status.active && status.validEmail === false && status.hasProfile === false;
termsField.parentElement.style.display = shouldHide ? 'none' : 'block';
}
});
}
rerender() {
/**
* active | 0 1 1 1
* validEmail | * 0 1 1
* hasProfile | * * 0 1
* --------------------------------
* defaultSubmitButton | 1 0 0 0
* defaultEmailField | 1 0 0 0
* defaultFormFields | 1 0 1 0
* extraFormFields | 0 0 0 1
* axoEmailField | 0 1 0 0
* axoProfileViews | 0 0 0 1
* axoPaymentContainer | 0 0 1 1
* axoSubmitButton | 0 0 1 1
*/
const scenario = this.identifyScenario(
this.status.active,
this.status.validEmail,
this.status.hasProfile
);
log('Scenario', scenario);
// Reset some elements to a default status.
this.el.watermarkContainer.hide();
if (scenario.defaultSubmitButton) {
this.el.defaultSubmitButton.show();
} else {
this.el.defaultSubmitButton.hide();
}
if (scenario.defaultEmailField) {
this.el.fieldBillingEmail.show();
} else {
this.el.fieldBillingEmail.hide();
}
if (scenario.defaultFormFields) {
this.el.customerDetails.show();
} else {
this.el.customerDetails.hide();
}
if (scenario.extraFormFields) {
this.el.customerDetails.show();
// Hiding of unwanted will be handled by the axoProfileViews handler.
}
if (scenario.axoEmailField) {
this.showAxoEmailField();
this.el.watermarkContainer.show();
// Move watermark to after email.
this.$(this.el.fieldBillingEmail.selector).append(
this.$(this.el.watermarkContainer.selector)
);
} else {
this.el.emailWidgetContainer.hide();
if (!scenario.defaultEmailField) {
this.el.fieldBillingEmail.hide();
}
}
if (scenario.axoProfileViews) {
this.el.billingAddressContainer.hide();
this.shippingView.activate();
this.billingView.activate();
this.cardView.activate();
// Move watermark to after shipping.
this.$(this.el.shippingAddressContainer.selector).after(
this.$(this.el.watermarkContainer.selector)
);
this.el.watermarkContainer.show();
} else {
this.shippingView.deactivate();
this.billingView.deactivate();
this.cardView.deactivate();
}
if (scenario.axoPaymentContainer) {
this.el.paymentContainer.show();
} else {
this.el.paymentContainer.hide();
}
if (scenario.axoSubmitButton) {
this.el.submitButtonContainer.show();
} else {
this.el.submitButtonContainer.hide();
}
this.ensureBillingFieldsConsistency();
this.ensureShippingFieldsConsistency();
}
identifyScenario(active, validEmail, hasProfile) {
let response = {
defaultSubmitButton: false,
defaultEmailField: false,
defaultFormFields: false,
extraFormFields: false,
axoEmailField: false,
axoProfileViews: false,
axoPaymentContainer: false,
axoSubmitButton: false,
}
if (active && validEmail && hasProfile) {
response.extraFormFields = true;
response.axoProfileViews = true;
response.axoPaymentContainer = true;
response.axoSubmitButton = true;
return response;
}
if (active && validEmail && !hasProfile) {
response.defaultFormFields = true;
response.axoEmailField = true;
response.axoPaymentContainer = true;
response.axoSubmitButton = true;
return response;
}
if (active && !validEmail) {
response.axoEmailField = true;
return response;
}
if (!active) {
response.defaultSubmitButton = true;
response.defaultEmailField = true;
response.defaultFormFields = true;
return response;
}
throw new Error('Invalid scenario.');
}
ensureBillingFieldsConsistency() {
const $billingFields = this.$('.woocommerce-billing-fields .form-row:visible');
const $billingHeaders = this.$('.woocommerce-billing-fields h3');
if (this.billingView.isActive()) {
if ($billingFields.length) {
$billingHeaders.show();
} else {
$billingHeaders.hide();
}
} else {
$billingHeaders.show();
}
}
ensureShippingFieldsConsistency() {
const $shippingFields = this.$('.woocommerce-shipping-fields .form-row:visible');
const $shippingHeaders = this.$('.woocommerce-shipping-fields h3');
if (this.shippingView.isActive()) {
if ($shippingFields.length) {
$shippingHeaders.show();
} else {
$shippingHeaders.hide();
}
} else {
$shippingHeaders.show();
}
}
showAxoEmailField() {
if (this.status.useEmailWidget) {
this.el.emailWidgetContainer.show();
this.el.fieldBillingEmail.hide();
} else {
this.el.emailWidgetContainer.hide();
this.el.fieldBillingEmail.show();
}
}
setStatus(key, value) {
this.status[key] = value;
log('Status updated', JSON.parse(JSON.stringify(this.status)));
document.dispatchEvent(new CustomEvent("axo_status_updated", {detail: this.status}));
this.rerender();
}
activateAxo() {
this.initPlacements();
this.initFastlane();
this.setStatus('active', true);
const emailInput = document.querySelector(this.el.fieldBillingEmail.selector + ' input');
if (emailInput && this.lastEmailCheckedIdentity !== emailInput.value) {
this.onChangeEmail();
}
}
deactivateAxo() {
this.setStatus('active', false);
}
initPlacements() {
const wrapper = this.el.axoCustomerDetails;
// Customer details container.
if (!document.querySelector(wrapper.selector)) {
document.querySelector(wrapper.anchorSelector).insertAdjacentHTML('afterbegin', `
<div id="${wrapper.id}" class="${wrapper.className}"></div>
`);
}
const wrapperElement = document.querySelector(wrapper.selector);
// Billing view container.
const bc = this.el.billingAddressContainer;
if (!document.querySelector(bc.selector)) {
wrapperElement.insertAdjacentHTML('beforeend', `
<div id="${bc.id}" class="${bc.className}"></div>
`);
}
// Shipping view container.
const sc = this.el.shippingAddressContainer;
if (!document.querySelector(sc.selector)) {
wrapperElement.insertAdjacentHTML('beforeend', `
<div id="${sc.id}" class="${sc.className}"></div>
`);
}
// Watermark container
const wc = this.el.watermarkContainer;
if (!document.querySelector(wc.selector)) {
this.emailInput = document.querySelector(this.el.fieldBillingEmail.selector + ' input');
this.emailInput.insertAdjacentHTML('afterend', `
<div class="${wc.className}" id="${wc.id}"></div>
`);
}
// Payment container
const pc = this.el.paymentContainer;
if (!document.querySelector(pc.selector)) {
const gatewayPaymentContainer = document.querySelector('.payment_method_ppcp-axo-gateway');
gatewayPaymentContainer.insertAdjacentHTML('beforeend', `
<div id="${pc.id}" class="${pc.className} hidden">
<div id="${pc.id}-form" class="${pc.className}-form"></div>
<div id="${pc.id}-details" class="${pc.className}-details"></div>
</div>
`);
}
if (this.useEmailWidget()) {
// Display email widget.
const ec = this.el.emailWidgetContainer;
if (!document.querySelector(ec.selector)) {
wrapperElement.insertAdjacentHTML('afterbegin', `
<div id="${ec.id}" class="${ec.className}">
--- EMAIL WIDGET PLACEHOLDER ---
</div>
`);
}
} else {
// Move email to the AXO container.
let emailRow = document.querySelector(this.el.fieldBillingEmail.selector);
wrapperElement.prepend(emailRow);
}
}
async initFastlane() {
if (this.initialized) {
return;
}
this.initialized = true;
await this.connect();
this.renderWatermark();
this.watchEmail();
}
async connect() {
if (this.axoConfig.environment.is_sandbox) {
window.localStorage.setItem('axoEnv', 'sandbox');
}
await this.fastlane.connect({
locale: this.locale,
styles: this.styles
});
this.fastlane.setLocale('en_us');
}
triggerGatewayChange() {
this.el.gatewayRadioButton.trigger('change');
}
async renderWatermark(includeAdditionalInfo = true) {
(await this.fastlane.FastlaneWatermarkComponent({
includeAdditionalInfo
})).render(this.el.watermarkContainer.selector);
}
watchEmail() {
if (this.useEmailWidget()) {
// TODO
} else {
this.emailInput = document.querySelector(this.el.fieldBillingEmail.selector + ' input');
this.emailInput.addEventListener('change', async ()=> {
this.onChangeEmail();
});
if (this.emailInput.value) {
this.onChangeEmail();
}
}
}
async onChangeEmail () {
this.clearData();
if (!this.status.active) {
log('Email checking skipped, AXO not active.');
return;
}
if (!this.emailInput) {
log('Email field not initialized.');
return;
}
log('Email changed: ' + (this.emailInput ? this.emailInput.value : '<empty>'));
this.$(this.el.paymentContainer.selector + '-detail').html('');
this.$(this.el.paymentContainer.selector + '-form').html('');
this.setStatus('validEmail', false);
this.setStatus('hasProfile', false);
this.hideGatewaySelection = false;
this.lastEmailCheckedIdentity = this.emailInput.value;
if (!this.emailInput.value || !this.emailInput.checkValidity()) {
log('The email address is not valid.');
return;
}
this.data.email = this.emailInput.value;
this.billingView.setData(this.data);
if (!this.fastlane.identity) {
log('Not initialized.');
return;
}
PayPalInsights.trackSubmitCheckoutEmail({
page_type: 'checkout'
});
await this.lookupCustomerByEmail();
}
async lookupCustomerByEmail() {
const lookupResponse = await this.fastlane.identity.lookupCustomerByEmail(this.emailInput.value);
if (lookupResponse.customerContextId) {
// Email is associated with a Connect profile or a PayPal member.
// Authenticate the customer to get access to their profile.
log('Email is associated with a Connect profile or a PayPal member');
const authResponse = await this.fastlane.identity.triggerAuthenticationFlow(lookupResponse.customerContextId);
log('AuthResponse', authResponse);
if (authResponse.authenticationState === 'succeeded') {
log(JSON.stringify(authResponse));
this.setShipping(authResponse.profileData.shippingAddress);
const billingAddress = authResponse.profileData?.card?.paymentSource?.card?.billingAddress;
if(billingAddress) {
this.setBilling({
address: billingAddress,
phoneNumber: authResponse.profileData.shippingAddress.phoneNumber.nationalNumber ?? ''
});
this.setCard(authResponse.profileData.card);
}
this.setStatus('validEmail', true);
this.setStatus('hasProfile', true);
this.hideGatewaySelection = true;
this.$('.wc_payment_methods label').hide();
await this.renderWatermark(false);
this.rerender();
} else {
// authentication failed or canceled by the customer
// set status as guest customer
log("Authentication Failed")
this.setStatus('validEmail', true);
this.setStatus('hasProfile', false);
await this.renderWatermark(true);
this.cardComponent = (await this.fastlane.FastlaneCardComponent(
this.cardComponentData()
)).render(this.el.paymentContainer.selector + '-form');
}
} else {
// No profile found with this email address.
// This is a guest customer.
log('No profile found with this email address.');
this.setStatus('validEmail', true);
this.setStatus('hasProfile', false);
await this.renderWatermark(true);
this.cardComponent = (await this.fastlane.FastlaneCardComponent(
this.cardComponentData()
)).render(this.el.paymentContainer.selector + '-form');
}
}
clearData() {
this.data = {
email: null,
billing: null,
shipping: null,
card: null,
};
}
setShipping(shipping) {
this.data.shipping = shipping;
this.shippingView.setData(this.data);
}
setBilling(billing) {
this.data.billing = billing;
this.billingView.setData(this.data);
}
setCard(card) {
this.data.card = card;
this.cardView.setData(this.data);
}
onClickSubmitButton() {
// TODO: validate data.
if (this.data.card) { // Ryan flow
log('Ryan flow.');
this.$('#ship-to-different-address-checkbox').prop('checked', 'checked');
let data = {};
this.billingView.toSubmitData(data);
this.shippingView.toSubmitData(data);
this.cardView.toSubmitData(data);
this.ensureBillingPhoneNumber(data);
this.submit(this.data.card.id, data);
} else { // Gary flow
log('Gary flow.');
try {
this.cardComponent.getPaymentToken(
this.tokenizeData()
).then((response) => {
this.submit(response.id);
});
} catch (e) {
log('Error tokenizing.');
alert('Error tokenizing data.');
}
}
}
cardComponentData() {
const fields = {
cardholderName: {
enabled: true
}
};
return {
fields: fields,
styles: this.deleteKeysWithEmptyString(this.axoConfig.style_options)
}
}
tokenizeData() {
return {
name: {
fullName: this.billingView.fullName()
},
billingAddress: {
addressLine1: this.billingView.inputValue('street1'),
addressLine2: this.billingView.inputValue('street2'),
adminArea1: this.billingView.inputValue('city'),
adminArea2: this.billingView.inputValue('stateCode'),
postalCode: this.billingView.inputValue('postCode'),
countryCode: this.billingView.inputValue('countryCode'),
}
}
}
submit(nonce, data) {
// Send the nonce and previously captured device data to server to complete checkout
if (!this.el.axoNonceInput.get()) {
this.$('form.woocommerce-checkout').append(`<input type="hidden" id="${this.el.axoNonceInput.id}" name="axo_nonce" value="" />`);
}
this.el.axoNonceInput.get().value = nonce;
PayPalInsights.trackEndCheckout({
amount: this.axoConfig?.insights?.amount,
page_type: 'checkout',
payment_method_selected: 'card',
user_data: {
country: 'US',
is_store_member: false,
}
});
if (data) {
// Ryan flow.
const form = document.querySelector('form.woocommerce-checkout');
const formData = new FormData(form);
this.showLoading();
// Fill in form data.
Object.keys(data).forEach((key) => {
formData.set(key, data[key]);
});
// Set type of user (Ryan) to be received on WC gateway process payment request.
formData.set('fastlane_member', true);
fetch(wc_checkout_params.checkout_url, { // TODO: maybe create a new endpoint to process_payment.
method: "POST",
body: formData
})
.then(response => response.json())
.then(responseData => {
if (responseData.result === 'failure') {
if (responseData.messages) {
const $notices = this.$('.woocommerce-notices-wrapper').eq(0);
$notices.html(responseData.messages);
this.$('html, body').animate({
scrollTop: $notices.offset().top
}, 500);
}
console.error('Failure:', responseData);
this.hideLoading();
return;
}
if (responseData.redirect) {
window.location.href = responseData.redirect;
}
})
.catch(error => {
console.error('Error:', error);
this.hideLoading();
});
} else {
// Gary flow.
this.el.defaultSubmitButton.click();
}
}
showLoading() {
const submitContainerSelector = '.woocommerce-checkout-payment';
jQuery('form.woocommerce-checkout').append('<div class="blockUI blockOverlay" style="z-index: 1000; border: medium; margin: 0px; padding: 0px; width: 100%; height: 100%; top: 0px; left: 0px; background: rgb(255, 255, 255); opacity: 0.6; cursor: default; position: absolute;"></div>');
disable(submitContainerSelector);
}
hideLoading() {
const submitContainerSelector = '.woocommerce-checkout-payment';
jQuery('form.woocommerce-checkout .blockOverlay').remove();
enable(submitContainerSelector);
}
useEmailWidget() {
return this.axoConfig?.widgets?.email === 'use_widget';
}
deleteKeysWithEmptyString = (obj) => {
for(let key of Object.keys(obj)){
if (obj[key] === ''){
delete obj[key];
}
else if (typeof obj[key] === 'object'){
obj[key] = this.deleteKeysWithEmptyString(obj[key]);
if (Object.keys(obj[key]).length === 0 ) delete obj[key];
}
}
return Array.isArray(obj) ? obj.filter(val => val) : obj;
}
ensureBillingPhoneNumber(data) {
if (data.billing_phone === '') {
let phone = '';
const cc = this.data?.shipping?.phoneNumber?.countryCode;
const number = this.data?.shipping?.phoneNumber?.nationalNumber;
if (cc) {
phone = `+${cc} `;
}
phone += number;
data.billing_phone = phone;
}
}
}
export default AxoManager;

View file

@ -0,0 +1,39 @@
class DomElement {
constructor(config) {
this.$ = jQuery;
this.config = config;
this.selector = this.config.selector;
this.id = this.config.id || null;
this.className = this.config.className || null;
this.attributes = this.config.attributes || null;
this.anchorSelector = this.config.anchorSelector || null;
}
trigger(action) {
this.$(this.selector).trigger(action);
}
on(action, callable) {
this.$(document).on(action, this.selector, callable);
}
hide() {
this.$(this.selector).hide();
}
show() {
this.$(this.selector).show();
}
click() {
this.get().click();
}
get() {
return document.querySelector(this.selector);
}
}
export default DomElement;

View file

@ -0,0 +1,95 @@
import DomElement from "./DomElement";
class DomElementCollection {
constructor() {
this.gatewayRadioButton = new DomElement({
selector: '#payment_method_ppcp-axo-gateway',
});
this.defaultSubmitButton = new DomElement({
selector: '#place_order',
});
this.paymentContainer = new DomElement({
id: 'ppcp-axo-payment-container',
selector: '#ppcp-axo-payment-container',
className: 'ppcp-axo-payment-container'
});
this.watermarkContainer = new DomElement({
id: 'ppcp-axo-watermark-container',
selector: '#ppcp-axo-watermark-container',
className: 'ppcp-axo-watermark-container'
});
this.customerDetails = new DomElement({
selector: '#customer_details > *:not(#ppcp-axo-customer-details)'
});
this.axoCustomerDetails = new DomElement({
id: 'ppcp-axo-customer-details',
selector: '#ppcp-axo-customer-details',
className: 'ppcp-axo-customer-details',
anchorSelector: '#customer_details'
});
this.emailWidgetContainer = new DomElement({
id: 'ppcp-axo-email-widget',
selector: '#ppcp-axo-email-widget',
className: 'ppcp-axo-email-widget'
});
this.shippingAddressContainer = new DomElement({
id: 'ppcp-axo-shipping-address-container',
selector: '#ppcp-axo-shipping-address-container',
className: 'ppcp-axo-shipping-address-container'
});
this.billingAddressContainer = new DomElement({
id: 'ppcp-axo-billing-address-container',
selector: '#ppcp-axo-billing-address-container',
className: 'ppcp-axo-billing-address-container'
});
this.fieldBillingEmail = new DomElement({
selector: '#billing_email_field'
});
this.submitButtonContainer = new DomElement({
selector: '#ppcp-axo-submit-button-container',
});
this.submitButton = new DomElement({
selector: '#ppcp-axo-submit-button-container button'
});
this.changeShippingAddressLink = new DomElement({
selector: '*[data-ppcp-axo-change-shipping-address]',
attributes: 'data-ppcp-axo-change-shipping-address',
});
this.changeBillingAddressLink = new DomElement({
selector: '*[data-ppcp-axo-change-billing-address]',
attributes: 'data-ppcp-axo-change-billing-address',
});
this.changeCardLink = new DomElement({
selector: '*[data-ppcp-axo-change-card]',
attributes: 'data-ppcp-axo-change-card',
});
this.showGatewaySelectionLink = new DomElement({
selector: '*[data-ppcp-axo-show-gateway-selection]',
attributes: 'data-ppcp-axo-show-gateway-selection',
});
this.axoNonceInput = new DomElement({
id: 'ppcp-axo-nonce',
selector: '#ppcp-axo-nonce',
});
}
}
export default DomElementCollection;

View file

@ -0,0 +1,153 @@
class FormFieldGroup {
constructor(config) {
this.data = {};
this.baseSelector = config.baseSelector;
this.contentSelector = config.contentSelector;
this.fields = config.fields || {};
this.template = config.template;
this.active = false;
}
setData(data) {
this.data = data;
this.refresh();
}
dataValue(fieldKey) {
if (!fieldKey || !this.fields[fieldKey]) {
return '';
}
if (typeof this.fields[fieldKey].valueCallback === 'function') {
return this.fields[fieldKey].valueCallback(this.data);
}
const path = this.fields[fieldKey].valuePath;
if (!path) {
return '';
}
const value = path.split('.').reduce((acc, key) => (acc && acc[key] !== undefined) ? acc[key] : undefined, this.data);
return value ? value : '';
}
activate() {
this.active = true;
this.refresh();
}
deactivate() {
this.active = false;
this.refresh();
}
toggle() {
this.active ? this.deactivate() : this.activate();
}
refresh() {
let content = document.querySelector(this.contentSelector);
if (!content) {
return;
}
content.innerHTML = '';
if (!this.active) {
this.hideField(this.contentSelector);
} else {
this.showField(this.contentSelector);
}
Object.keys(this.fields).forEach((key) => {
const field = this.fields[key];
if (this.active && !field.showInput) {
this.hideField(field.selector);
} else {
this.showField(field.selector);
}
});
if (typeof this.template === 'function') {
content.innerHTML = this.template({
value: (fieldKey) => {
return this.dataValue(fieldKey);
},
isEmpty: () => {
let isEmpty = true;
Object.keys(this.fields).forEach((fieldKey) => {
if (this.dataValue(fieldKey)) {
isEmpty = false;
return false;
}
});
return isEmpty;
}
});
}
}
showField(selector) {
const field = document.querySelector(this.baseSelector + ' ' + selector);
if (field) {
field.classList.remove('ppcp-axo-field-hidden');
}
}
hideField(selector) {
const field = document.querySelector(this.baseSelector + ' ' + selector);
if (field) {
field.classList.add('ppcp-axo-field-hidden');
}
}
inputElement(name) {
const baseSelector = this.fields[name].selector;
const select = document.querySelector(baseSelector + ' select');
if (select) {
return select;
}
const input = document.querySelector(baseSelector + ' input');
if (input) {
return input;
}
return null;
}
inputValue(name) {
const el = this.inputElement(name);
return el ? el.value : '';
}
toSubmitData(data) {
Object.keys(this.fields).forEach((fieldKey) => {
const field = this.fields[fieldKey];
if (!field.valuePath || !field.selector) {
return true;
}
const inputElement = this.inputElement(fieldKey);
if (!inputElement) {
return true;
}
data[inputElement.name] = this.dataValue(fieldKey);
});
}
}
export default FormFieldGroup;

View file

@ -0,0 +1,42 @@
class Fastlane {
construct() {
this.connection = null;
this.identity = null;
this.profile = null;
this.FastlaneCardComponent = null;
this.FastlanePaymentComponent = null;
this.FastlaneWatermarkComponent = null;
}
connect(config) {
return new Promise((resolve, reject) => {
window.paypal.Fastlane(config)
.then((result) => {
this.init(result);
resolve();
})
.catch((error) => {
console.error(error)
reject();
});
});
}
init(connection) {
this.connection = connection;
this.identity = this.connection.identity;
this.profile = this.connection.profile;
this.FastlaneCardComponent = this.connection.FastlaneCardComponent;
this.FastlanePaymentComponent = this.connection.FastlanePaymentComponent;
this.FastlaneWatermarkComponent = this.connection.FastlaneWatermarkComponent
}
setLocale(locale) {
this.connection.setLocale(locale);
}
}
export default Fastlane;

View file

@ -0,0 +1,4 @@
export function log(...args) {
//console.log('[AXO] ', ...args);
}

View file

@ -0,0 +1,58 @@
class PayPalInsights {
constructor() {
window.paypalInsightDataLayer = window.paypalInsightDataLayer || [];
document.paypalInsight = () => {
paypalInsightDataLayer.push(arguments);
}
}
/**
* @returns {PayPalInsights}
*/
static init() {
if (!PayPalInsights.instance) {
PayPalInsights.instance = new PayPalInsights();
}
return PayPalInsights.instance;
}
static track(eventName, data) {
PayPalInsights.init();
paypalInsight('event', eventName, data);
}
static config (clientId, data) {
PayPalInsights.init();
paypalInsight('config', clientId, data);
}
static setSessionId (sessionId) {
PayPalInsights.init();
paypalInsight('set', { session_id: sessionId });
}
static trackJsLoad () {
PayPalInsights.track('js_load', { timestamp: Date.now() });
}
static trackBeginCheckout (data) {
PayPalInsights.track('begin_checkout', data);
}
static trackSubmitCheckoutEmail (data) {
PayPalInsights.track('submit_checkout_email', data);
}
static trackSelectPaymentMethod (data) {
PayPalInsights.track('select_payment_method', data);
}
static trackEndCheckout (data) {
PayPalInsights.track('end_checkout', data);
}
}
export default PayPalInsights;

View file

@ -0,0 +1,136 @@
import FormFieldGroup from "../Components/FormFieldGroup";
class BillingView {
constructor(selector, elements) {
this.el = elements;
this.group = new FormFieldGroup({
baseSelector: '.woocommerce-checkout',
contentSelector: selector,
template: (data) => {
const valueOfSelect = (selectSelector, key) => {
if (!key) {
return '';
}
const selectElement = document.querySelector(selectSelector);
if (!selectElement) {
return key;
}
const option = selectElement.querySelector(`option[value="${key}"]`);
return option ? option.textContent : key;
}
if (data.isEmpty()) {
return `
<div style="margin-bottom: 20px;">
<div class="axo-checkout-header-section">
<h3>Billing</h3>
<a href="javascript:void(0)" ${this.el.changeBillingAddressLink.attributes}>Edit</a>
</div>
<div>Please fill in your billing details.</div>
</div>
`;
}
return `
<div style="margin-bottom: 20px;">
<div class="axo-checkout-header-section">
<h3>Billing</h3>
<a href="javascript:void(0)" ${this.el.changeBillingAddressLink.attributes}>Edit</a>
</div>
<div>${data.value('email')}</div>
<div>${data.value('company')}</div>
<div>${data.value('firstName')} ${data.value('lastName')}</div>
<div>${data.value('street1')}</div>
<div>${data.value('street2')}</div>
<div>${data.value('postCode')} ${data.value('city')}</div>
<div>${valueOfSelect('#billing_state', data.value('stateCode'))}</div>
<div>${valueOfSelect('#billing_country', data.value('countryCode'))}</div>
</div>
`;
},
fields: {
email: {
'valuePath': 'email',
},
firstName: {
'selector': '#billing_first_name_field',
'valuePath': null
},
lastName: {
'selector': '#billing_last_name_field',
'valuePath': null
},
street1: {
'selector': '#billing_address_1_field',
'valuePath': 'billing.address.addressLine1',
},
street2: {
'selector': '#billing_address_2_field',
'valuePath': null
},
postCode: {
'selector': '#billing_postcode_field',
'valuePath': 'billing.address.postalCode',
},
city: {
'selector': '#billing_city_field',
'valuePath': 'billing.address.adminArea2',
},
stateCode: {
'selector': '#billing_state_field',
'valuePath': 'billing.address.adminArea1',
},
countryCode: {
'selector': '#billing_country_field',
'valuePath': 'billing.address.countryCode',
},
company: {
'selector': '#billing_company_field',
'valuePath': null,
},
phone: {
'selector': '#billing_phone_field',
'valuePath': 'billing.phoneNumber'
}
}
});
}
isActive() {
return this.group.active;
}
activate() {
this.group.activate();
}
deactivate() {
this.group.deactivate();
}
refresh() {
this.group.refresh();
}
setData(data) {
this.group.setData(data);
}
inputValue(name) {
return this.group.inputValue(name);
}
fullName() {
return `${this.inputValue('firstName')} ${this.inputValue('lastName')}`.trim();
}
toSubmitData(data) {
return this.group.toSubmitData(data);
}
}
export default BillingView;

View file

@ -0,0 +1,117 @@
import FormFieldGroup from "../Components/FormFieldGroup";
class CardView {
constructor(selector, elements, manager) {
this.el = elements;
this.manager = manager;
this.group = new FormFieldGroup({
baseSelector: '.ppcp-axo-payment-container',
contentSelector: selector,
template: (data) => {
const selectOtherPaymentMethod = () => {
if (!this.manager.hideGatewaySelection) {
return '';
}
return `<p style="margin-top: 40px; text-align: center;"><a href="javascript:void(0)" ${this.el.showGatewaySelectionLink.attributes}>Select other payment method</a></p>`;
};
if (data.isEmpty()) {
return `
<div style="margin-bottom: 20px; text-align: center;">
<div style="border:2px solid #cccccc; border-radius: 10px; padding: 26px 20px; margin-bottom: 20px; background-color:#f6f6f6">
<div>Please fill in your card details.</div>
</div>
<h4><a href="javascript:void(0)" ${this.el.changeCardLink.attributes}>Add card details</a></h4>
${selectOtherPaymentMethod()}
</div>
`;
}
const expiry = data.value('expiry').split('-');
const cardIcons = {
'VISA': 'visa-dark.svg',
'MASTER_CARD': 'mastercard-dark.svg',
'AMEX': 'amex.svg',
'DISCOVER': 'discover.svg',
};
return `
<div style="margin-bottom: 20px;">
<div class="axo-checkout-header-section">
<h3>Card Details</h3>
<a href="javascript:void(0)" ${this.el.changeCardLink.attributes}>Edit</a>
</div>
<div style="border:2px solid #cccccc; border-radius: 10px; padding: 16px 20px; background-color:#f6f6f6">
<div style="float: right;">
<img
class="ppcp-card-icon"
title="${data.value('brand')}"
src="/wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-wc-gateway/assets/images/${cardIcons[data.value('brand')]}"
alt="${data.value('brand')}"
>
</div>
<div style="font-family: monospace; font-size: 1rem; margin-top: 10px;">${data.value('lastDigits') ? '**** **** **** ' + data.value('lastDigits'): ''}</div>
<div>${expiry[1]}/${expiry[0]}</div>
<div style="text-transform: uppercase">${data.value('name')}</div>
</div>
${selectOtherPaymentMethod()}
</div>
`;
},
fields: {
brand: {
'valuePath': 'card.paymentSource.card.brand',
},
expiry: {
'valuePath': 'card.paymentSource.card.expiry',
},
lastDigits: {
'valuePath': 'card.paymentSource.card.lastDigits',
},
name: {
'valuePath': 'card.paymentSource.card.name',
},
}
});
}
activate() {
this.group.activate();
}
deactivate() {
this.group.deactivate();
}
refresh() {
this.group.refresh();
}
setData(data) {
this.group.setData(data);
}
toSubmitData(data) {
const name = this.group.dataValue('name');
const { firstName, lastName } = this.splitName(name);
data['billing_first_name'] = firstName;
data['billing_last_name'] = lastName ? lastName : firstName;
return this.group.toSubmitData(data);
}
splitName(fullName) {
let nameParts = fullName.trim().split(' ');
let firstName = nameParts[0];
let lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : '';
return { firstName, lastName };
}
}
export default CardView;

View file

@ -0,0 +1,143 @@
import FormFieldGroup from "../Components/FormFieldGroup";
class ShippingView {
constructor(selector, elements) {
this.el = elements;
this.group = new FormFieldGroup({
baseSelector: '.woocommerce-checkout',
contentSelector: selector,
template: (data) => {
const valueOfSelect = (selectSelector, key) => {
if (!key) {
return '';
}
const selectElement = document.querySelector(selectSelector);
if (!selectElement) {
return key;
}
const option = selectElement.querySelector(`option[value="${key}"]`);
return option ? option.textContent : key;
}
if (data.isEmpty()) {
return `
<div style="margin-bottom: 20px;">
<div class="axo-checkout-header-section">
<h3>Shipping</h3>
<a href="javascript:void(0)" ${this.el.changeShippingAddressLink.attributes}>Edit</a>
</div>
<div>Please fill in your shipping details.</div>
</div>
`;
}
return `
<div style="margin-bottom: 20px;">
<div class="axo-checkout-header-section">
<h3>Shipping</h3>
<a href="javascript:void(0)" ${this.el.changeShippingAddressLink.attributes}>Edit</a>
</div>
<div>${data.value('email')}</div>
<div>${data.value('company')}</div>
<div>${data.value('firstName')} ${data.value('lastName')}</div>
<div>${data.value('street1')}</div>
<div>${data.value('street2')}</div>
<div>${data.value('city')}, ${valueOfSelect('#billing_state', data.value('stateCode'))} ${data.value('postCode')} </div>
<div>${valueOfSelect('#billing_country', data.value('countryCode'))}</div>
<div>${data.value('phone')}</div>
</div>
`;
},
fields: {
email: {
'valuePath': 'email',
},
firstName: {
'key': 'firstName',
'selector': '#shipping_first_name_field',
'valuePath': 'shipping.name.firstName',
},
lastName: {
'selector': '#shipping_last_name_field',
'valuePath': 'shipping.name.lastName',
},
street1: {
'selector': '#shipping_address_1_field',
'valuePath': 'shipping.address.addressLine1',
},
street2: {
'selector': '#shipping_address_2_field',
'valuePath': null
},
postCode: {
'selector': '#shipping_postcode_field',
'valuePath': 'shipping.address.postalCode',
},
city: {
'selector': '#shipping_city_field',
'valuePath': 'shipping.address.adminArea2',
},
stateCode: {
'selector': '#shipping_state_field',
'valuePath': 'shipping.address.adminArea1',
},
countryCode: {
'selector': '#shipping_country_field',
'valuePath': 'shipping.address.countryCode',
},
company: {
'selector': '#shipping_company_field',
'valuePath': null,
},
shipDifferentAddress: {
'selector': '#ship-to-different-address',
'valuePath': null,
},
phone: {
//'selector': '#billing_phone_field', // There is no shipping phone field.
'valueCallback': function (data) {
let phone = '';
const cc = data?.shipping?.phoneNumber?.countryCode;
const number = data?.shipping?.phoneNumber?.nationalNumber;
if (cc) {
phone = `+${cc} `;
}
phone += number;
return phone;
}
}
}
});
}
isActive() {
return this.group.active;
}
activate() {
this.group.activate();
}
deactivate() {
this.group.deactivate();
}
refresh() {
this.group.refresh();
}
setData(data) {
this.group.setData(data);
}
toSubmitData(data) {
return this.group.toSubmitData(data);
}
}
export default ShippingView;

View file

@ -0,0 +1,33 @@
import AxoManager from "./AxoManager";
import {loadPaypalScript} from "../../../ppcp-button/resources/js/modules/Helper/ScriptLoading";
(function ({
axoConfig,
ppcpConfig,
jQuery
}) {
const bootstrap = () => {
new AxoManager(axoConfig, ppcpConfig);
}
document.addEventListener(
'DOMContentLoaded',
() => {
if (!typeof (PayPalCommerceGateway)) {
console.error('AXO could not be configured.');
return;
}
// Load PayPal
loadPaypalScript(ppcpConfig, () => {
bootstrap();
});
},
);
})({
axoConfig: window.wc_ppcp_axo,
ppcpConfig: window.PayPalCommerceGateway,
jQuery: window.jQuery
});

View file

@ -0,0 +1,148 @@
<?php
/**
* The Axo module services.
*
* @package WooCommerce\PayPalCommerce\Axo
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo;
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// If AXO can be configured.
'axo.eligible' => static function ( ContainerInterface $container ): bool {
$apm_applies = $container->get( 'axo.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country_currency() && $apm_applies->for_settings();
},
'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
$container->get( 'axo.supported-country-currency-matrix' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
// If AXO is configured and onboarded.
'axo.available' => static function ( ContainerInterface $container ): bool {
return true;
},
'axo.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
}
return plugins_url(
'/modules/ppcp-axo/',
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'axo.manager' => static function ( ContainerInterface $container ): AxoManager {
return new AxoManager(
$container->get( 'axo.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'session.handler' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.settings.status' ),
$container->get( 'api.shop.currency' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'axo.gateway' => static function ( ContainerInterface $container ): AxoGateway {
return new AxoGateway(
$container->get( 'wcgateway.settings.render' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.url' ),
$container->get( 'wcgateway.order-processor' ),
$container->get( 'axo.card_icons' ),
$container->get( 'axo.card_icons.axo' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.factory.shipping-preference' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'onboarding.environment' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'axo.card_icons' => static function ( ContainerInterface $container ): array {
return array(
array(
'title' => 'Visa',
'file' => 'visa-dark.svg',
),
array(
'title' => 'MasterCard',
'file' => 'mastercard-dark.svg',
),
array(
'title' => 'American Express',
'file' => 'amex.svg',
),
array(
'title' => 'Discover',
'file' => 'discover.svg',
),
);
},
'axo.card_icons.axo' => static function ( ContainerInterface $container ): array {
return array(
array(
'title' => 'Dinersclub',
'file' => 'dinersclub-light.svg',
),
array(
'title' => 'Discover',
'file' => 'discover-light.svg',
),
array(
'title' => 'JCB',
'file' => 'jcb-light.svg',
),
array(
'title' => 'MasterCard',
'file' => 'mastercard-light.svg',
),
array(
'title' => 'UnionPay',
'file' => 'unionpay-light.svg',
),
array(
'title' => 'Visa',
'file' => 'visa-light.svg',
),
);
},
/**
* The matrix which countries and currency combinations can be used for AXO.
*/
'axo.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for AXO.
*/
return apply_filters(
'woocommerce_paypal_payments_axo_supported_country_currency_matrix',
array(
'US' => array(
'USD',
),
)
);
},
);

View file

@ -0,0 +1,230 @@
<?php
/**
* The AXO AxoManager
*
* @package WooCommerce\PayPalCommerce\WcGateway\Assets
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Assets;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class AxoManager.
*
* @param string $module_url The URL to the module.
*/
class AxoManager {
/**
* The URL to the module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The environment object.
*
* @var Environment
*/
private $environment;
/**
* The Settings status helper.
*
* @var SettingsStatus
*/
private $settings_status;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* AxoManager constructor.
*
* @param string $module_url The URL to the module.
* @param string $version The assets version.
* @param SessionHandler $session_handler The Session handler.
* @param Settings $settings The Settings.
* @param Environment $environment The environment object.
* @param SettingsStatus $settings_status The Settings status helper.
* @param string $currency 3-letter currency code of the shop.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $module_url,
string $version,
SessionHandler $session_handler,
Settings $settings,
Environment $environment,
SettingsStatus $settings_status,
string $currency,
LoggerInterface $logger
) {
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->environment = $environment;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->logger = $logger;
}
/**
* Enqueues scripts/styles.
*
* @return void
*/
public function enqueue() {
// Register styles.
wp_register_style(
'wc-ppcp-axo',
untrailingslashit( $this->module_url ) . '/assets/css/styles.css',
array(),
$this->version
);
wp_enqueue_style( 'wc-ppcp-axo' );
// Register scripts.
wp_register_script(
'wc-ppcp-axo',
untrailingslashit( $this->module_url ) . '/assets/js/boot.js',
array(),
$this->version,
true
);
wp_enqueue_script( 'wc-ppcp-axo' );
wp_localize_script(
'wc-ppcp-axo',
'wc_ppcp_axo',
$this->script_data()
);
}
/**
* The configuration for AXO.
*
* @return array
*/
private function script_data() {
return array(
'environment' => array(
'is_sandbox' => $this->environment->current_environment() === 'sandbox',
),
'widgets' => array(
'email' => 'render',
),
'insights' => array(
'enabled' => true,
'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ),
'session_id' =>
substr(
method_exists( WC()->session, 'get_customer_unique_id' ) ? md5( WC()->session->get_customer_unique_id() ) : '',
0,
16
),
'amount' => array(
'currency_code' => get_woocommerce_currency(),
'value' => WC()->cart->get_total( 'numeric' ),
),
),
'style_options' => array(
'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '',
'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '',
'fontFamily' => $this->settings->has( 'axo_style_root_font_family' ) ? $this->settings->get( 'axo_style_root_font_family' ) : '',
'textColorBase' => $this->settings->has( 'axo_style_root_text_color_base' ) ? $this->settings->get( 'axo_style_root_text_color_base' ) : '',
'fontSizeBase' => $this->settings->has( 'axo_style_root_font_size_base' ) ? $this->settings->get( 'axo_style_root_font_size_base' ) : '',
'padding' => $this->settings->has( 'axo_style_root_padding' ) ? $this->settings->get( 'axo_style_root_padding' ) : '',
'primaryColor' => $this->settings->has( 'axo_style_root_primary_color' ) ? $this->settings->get( 'axo_style_root_primary_color' ) : '',
),
'input' => array(
'backgroundColor' => $this->settings->has( 'axo_style_input_bg_color' ) ? $this->settings->get( 'axo_style_input_bg_color' ) : '',
'borderRadius' => $this->settings->has( 'axo_style_input_border_radius' ) ? $this->settings->get( 'axo_style_input_border_radius' ) : '',
'borderColor' => $this->settings->has( 'axo_style_input_border_color' ) ? $this->settings->get( 'axo_style_input_border_color' ) : '',
'borderWidth' => $this->settings->has( 'axo_style_input_border_width' ) ? $this->settings->get( 'axo_style_input_border_width' ) : '',
'textColorBase' => $this->settings->has( 'axo_style_input_text_color_base' ) ? $this->settings->get( 'axo_style_input_text_color_base' ) : '',
'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '',
),
),
'name_on_card' => $this->settings->has( 'axo_name_on_card' ) ? $this->settings->get( 'axo_name_on_card' ) : '',
);
}
/**
* Returns the action name that PayPal AXO button will use for rendering on the checkout page.
*
* @return string
*/
public function checkout_button_renderer_hook(): string {
/**
* The filter returning the action name that PayPal AXO button will use for rendering on the checkout page.
*/
return (string) apply_filters( 'woocommerce_paypal_payments_checkout_axo_renderer_hook', 'woocommerce_review_order_after_submit' );
}
/**
* Renders the HTML for the AXO submit button.
*/
public function render_checkout_button(): void {
$id = 'ppcp-axo-submit-button-container';
/**
* The WC filter returning the WC order button text.
* phpcs:disable WordPress.WP.I18n.TextDomainMismatch
*/
$label = apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) );
printf(
'<div id="%1$s" style="display: none;">
<button type="button" class="button alt ppcp-axo-order-button">%2$s</button>
</div>',
esc_attr( $id ),
esc_html( $label )
);
}
}

View file

@ -0,0 +1,269 @@
<?php
/**
* The Axo module.
*
* @package WooCommerce\PayPalCommerce\Axo
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo;
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\Axo\Assets\AxoManager;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class AxoModule
*/
class AxoModule implements ModuleInterface {
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
$module = $this;
add_filter(
'woocommerce_payment_gateways',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ): array {
if ( ! is_array( $methods ) ) {
return $methods;
}
$gateway = $c->get( 'axo.gateway' );
// Check if the module is applicable, correct country, currency, ... etc.
if ( ! $c->get( 'axo.eligible' ) ) {
return $methods;
}
// Add the gateway in admin area.
if ( is_admin() ) {
$methods[] = $gateway;
return $methods;
}
if ( is_user_logged_in() ) {
return $methods;
}
$methods[] = $gateway;
return $methods;
},
1,
9
);
// Hides credit card gateway on checkout when using Fastlane.
add_filter(
'woocommerce_available_payment_gateways',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ): array {
if ( ! is_array( $methods ) || ! $c->get( 'axo.eligible' ) ) {
return $methods;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( apply_filters(
'woocommerce_paypal_payments_axo_hide_credit_card_gateway',
$this->hide_credit_card_when_using_fastlane( $methods, $settings )
) ) {
unset( $methods[ CreditCardGateway::ID ] );
}
return $methods;
}
);
add_action(
'init',
static function () use ( $c, $module ) {
// Check if the module is applicable, correct country, currency, ... etc.
if ( ! $c->get( 'axo.eligible' ) ) {
return;
}
$manager = $c->get( 'axo.manager' );
assert( $manager instanceof AxoManager );
// Enqueue frontend scripts.
add_action(
'wp_enqueue_scripts',
static function () use ( $c, $manager ) {
$smart_button = $c->get( 'button.smart-button' );
assert( $smart_button instanceof SmartButtonInterface );
if ( $smart_button->should_load_ppcp_script() ) {
$manager->enqueue();
}
}
);
// Render submit button.
add_action(
$manager->checkout_button_renderer_hook(),
static function () use ( $c, $manager ) {
$manager->render_checkout_button();
}
);
/**
* 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(
'wp_head',
function () {
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
echo '<script async src="https://www.paypalobjects.com/insights/v1/paypal-insights.sandbox.min.js"></script>';
}
);
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function( array $localized_script_data ) use ( $c, $module ) {
$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 );
}
);
add_filter(
'ppcp_onboarding_dcc_table_rows',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $rows, $renderer ): array {
if ( ! is_array( $rows ) ) {
return $rows;
}
if ( $renderer instanceof OnboardingOptionsRenderer ) {
$rows[] = $renderer->render_table_row(
__( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ),
__( 'Yes', 'woocommerce-paypal-payments' ),
__( 'Help accelerate guest checkout with PayPal\'s autofill solution.', 'woocommerce-paypal-payments' )
);
}
return $rows;
},
10,
2
);
},
1
);
}
/**
* 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 {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( ! $target_customer_id ) {
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
}
}
$sdk_client_token = $api->sdk_client_token( $target_customer_id );
$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;
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
/**
* Condition to evaluate if Credit Card gateway should be hidden.
*
* @param array $methods WC payment methods.
* @param Settings $settings The settings.
* @return bool
*/
private function hide_credit_card_when_using_fastlane( array $methods, Settings $settings ): bool {
$is_axo_enabled = $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) ?? false;
return ! is_admin()
&& is_user_logged_in() === false
&& isset( $methods[ CreditCardGateway::ID ] )
&& $is_axo_enabled;
}
}

View file

@ -0,0 +1,363 @@
<?php
/**
* The AXO Gateway
*
* @package WooCommerce\PayPalCommerce\WcGateway\Gateway
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Gateway;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
/**
* Class AXOGateway.
*/
class AxoGateway extends WC_Payment_Gateway {
use OrderMetaTrait, GatewaySettingsRendererTrait;
const ID = 'ppcp-axo-gateway';
/**
* The Settings Renderer.
*
* @var SettingsRenderer
*/
protected $settings_renderer;
/**
* The settings.
*
* @var ContainerInterface
*/
protected $ppcp_settings;
/**
* The WcGateway module URL.
*
* @var string
*/
protected $wcgateway_module_url;
/**
* The processor for orders.
*
* @var OrderProcessor
*/
protected $order_processor;
/**
* The card icons.
*
* @var array
*/
protected $card_icons;
/**
* The AXO card icons.
*
* @var array
*/
protected $card_icons_axo;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
protected $order_endpoint;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
protected $purchase_unit_factory;
/**
* The shipping preference factory.
*
* @var ShippingPreferenceFactory
*/
protected $shipping_preference_factory;
/**
* The transaction url provider.
*
* @var TransactionUrlProvider
*/
protected $transaction_url_provider;
/**
* The environment.
*
* @var Environment
*/
protected $environment;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* AXOGateway constructor.
*
* @param SettingsRenderer $settings_renderer The settings renderer.
* @param ContainerInterface $ppcp_settings The settings.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param OrderProcessor $order_processor The Order processor.
* @param array $card_icons The card icons.
* @param array $card_icons_axo The card icons.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param ShippingPreferenceFactory $shipping_preference_factory The shipping preference factory.
* @param TransactionUrlProvider $transaction_url_provider The transaction url provider.
* @param Environment $environment The environment.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SettingsRenderer $settings_renderer,
ContainerInterface $ppcp_settings,
string $wcgateway_module_url,
OrderProcessor $order_processor,
array $card_icons,
array $card_icons_axo,
OrderEndpoint $order_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
ShippingPreferenceFactory $shipping_preference_factory,
TransactionUrlProvider $transaction_url_provider,
Environment $environment,
LoggerInterface $logger
) {
$this->id = self::ID;
$this->settings_renderer = $settings_renderer;
$this->ppcp_settings = $ppcp_settings;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->order_processor = $order_processor;
$this->card_icons = $card_icons;
$this->card_icons_axo = $card_icons_axo;
$this->method_title = __( 'Fastlane Debit & Credit Cards', 'woocommerce-paypal-payments' );
$this->method_description = __( 'PayPal Fastlane offers an accelerated checkout experience that recognizes guest shoppers and autofills their details so they can pay in seconds.', 'woocommerce-paypal-payments' );
$is_axo_enabled = $this->ppcp_settings->has( 'axo_enabled' ) && $this->ppcp_settings->get( 'axo_enabled' );
$this->update_option( 'enabled', $is_axo_enabled ? 'yes' : 'no' );
$this->title = $this->ppcp_settings->has( 'axo_gateway_title' )
? $this->ppcp_settings->get( 'axo_gateway_title' )
: $this->get_option( 'title', $this->method_title );
$this->description = $this->get_option( 'description', '' );
$this->init_form_fields();
$this->init_settings();
add_action(
'woocommerce_update_options_payment_gateways_' . $this->id,
array(
$this,
'process_admin_options',
)
);
$this->order_endpoint = $order_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->shipping_preference_factory = $shipping_preference_factory;
$this->logger = $logger;
$this->transaction_url_provider = $transaction_url_provider;
$this->environment = $environment;
}
/**
* Initialize the form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'AXO', 'woocommerce-paypal-payments' ),
'default' => 'no',
'desc_tip' => true,
'description' => __( 'Enable/Disable AXO payment gateway.', 'woocommerce-paypal-payments' ),
),
'ppcp' => array(
'type' => 'ppcp',
),
);
}
/**
* Processes the order.
*
* @param int $order_id The WC order ID.
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) );
if ( $fastlane_member ) {
$payment_method_title = __( 'Debit & Credit Cards (via Fastlane by PayPal)', 'woocommerce-paypal-payments' );
$wc_order->set_payment_method_title( $payment_method_title );
$wc_order->save();
}
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
try {
$shipping_preference = $this->shipping_preference_factory->from_state(
$purchase_unit,
'checkout'
);
$payment_source_properties = new \stdClass();
$payment_source_properties->single_use_token = $nonce;
$payment_source = new PaymentSource(
'card',
$payment_source_properties
);
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
null,
null,
'',
ApplicationContext::USER_ACTION_CONTINUE,
'',
array(),
$payment_source
);
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( $error );
wc_add_notice( $error, 'error' );
$wc_order->update_status(
'failed',
$error
);
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
}
WC()->cart->empty_cart();
$result = array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
return $result;
}
/**
* Returns the icons of the gateway.
*
* @return string
*/
public function get_icon() {
$icon = parent::get_icon();
$icons = $this->card_icons;
$icons_src = esc_url( $this->wcgateway_module_url ) . 'assets/images/';
if ( empty( $this->card_icons ) ) {
return $icon;
}
$images = array();
foreach ( $icons as $card ) {
$images[] = '<img
class="ppcp-card-icon"
title="' . $card['title'] . '"
src="' . $icons_src . $card['file'] . '"
> ';
}
return implode( '', $images );
}
/**
* 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 );
}
/**
* Return the gateway's title.
*
* @return string
*/
public function get_title() {
if ( is_admin() ) {
// $theorder and other things for retrieving the order or post info are not available
// in the constructor, so must do it here.
global $theorder;
if ( $theorder instanceof WC_Order ) {
if ( $theorder->get_payment_method() === self::ID ) {
$payment_method_title = $theorder->get_payment_method_title();
if ( $payment_method_title ) {
$this->title = $payment_method_title;
}
}
}
}
return parent::get_title();
}
/**
* Returns the settings renderer.
*
* @return SettingsRenderer
*/
protected function settings_renderer(): SettingsRenderer {
return $this->settings_renderer;
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* ApmApplies helper.
* Checks if AXO is available for a given country and currency.
*
* @package WooCommerce\PayPalCommerce\Axo\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Helper;
/**
* Class ApmApplies
*/
class ApmApplies {
/**
* The matrix which countries and currency combinations can be used for AXO.
*
* @var array
*/
private $allowed_country_currency_matrix;
/**
* 3-letter currency code of the shop.
*
* @var string
*/
private $currency;
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
/**
* DccApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for AXO.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
array $allowed_country_currency_matrix,
string $currency,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
}
/**
* Returns whether AXO can be used in the current country and the current currency used.
*
* @return bool
*/
public function for_country_currency(): bool {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
}
/**
* Returns whether the settings are compatible with AXO.
*
* @return bool
*/
public function for_settings(): bool {
if ( get_option( 'woocommerce_ship_to_destination' ) === 'billing_only' ) { // Force shipping to the customer billing address.
return false;
}
return true;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* Properties of the AXO module.
*
* @package WooCommerce\PayPalCommerce\Axo\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Helper;
/**
* Class PropertiesDictionary
*/
class PropertiesDictionary {
/**
* Returns the list of possible privacy options.
*
* @return array
*/
public static function privacy_options(): array {
return array(
'yes' => __( 'Yes (Recommended)', 'woocommerce-paypal-payments' ),
'no' => __( 'No', 'woocommerce-paypal-payments' ),
);
}
}

View file

@ -0,0 +1,39 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'boot': path.resolve('./resources/js/boot.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'}
]
}]
}
};

2213
modules/ppcp-axo/yarn.lock Normal file

File diff suppressed because it is too large Load diff

156
modules/ppcp-blocks/composer.lock generated Normal file
View file

@ -0,0 +1,156 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "30c5bd428bece98b555ddc0b2da044f3",
"packages": [
{
"name": "container-interop/service-provider",
"version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/container-interop/service-provider.git",
"reference": "e04441ca21ef03e10dce70b0af29269281eec6dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/container-interop/service-provider/zipball/e04441ca21ef03e10dce70b0af29269281eec6dc",
"reference": "e04441ca21ef03e10dce70b0af29269281eec6dc",
"shasum": ""
},
"require": {
"psr/container": "^1.0 || ^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Interop\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Promoting container interoperability through standard service providers",
"homepage": "https://github.com/container-interop/service-provider",
"support": {
"issues": "https://github.com/container-interop/service-provider/issues",
"source": "https://github.com/container-interop/service-provider/tree/v0.4.1"
},
"time": "2023-12-14T14:50:12+00:00"
},
{
"name": "dhii/module-interface",
"version": "v0.3.0-alpha2",
"source": {
"type": "git",
"url": "https://github.com/Dhii/module-interface.git",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dhii/module-interface/zipball/0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"shasum": ""
},
"require": {
"container-interop/service-provider": "^0.4",
"php": "^7.1 | ^8.0",
"psr/container": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 | ^8.0 | ^9.0",
"slevomat/coding-standard": "^6.0",
"vimeo/psalm": "^3.11.7 | ^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-develop": "0.3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dhii\\Modular\\Module\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dhii Team",
"email": "development@dhii.co"
}
],
"description": "Interfaces for modules",
"support": {
"issues": "https://github.com/Dhii/module-interface/issues",
"source": "https://github.com/Dhii/module-interface/tree/v0.3.0-alpha2"
},
"time": "2021-08-23T08:23:01+00:00"
},
{
"name": "psr/container",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.2"
},
"time": "2021-11-05T16:50:12+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^7.2 | ^8.0"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -10,13 +10,16 @@
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0"
"@paypal/react-paypal-js": "^8.2.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",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",

View file

@ -16,6 +16,7 @@ import {
import {
loadPaypalScriptPromise
} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";
import {
normalizeStyleForFundingSource
} from '../../../ppcp-button/resources/js/modules/Helper/Style'
@ -120,8 +121,13 @@ const PayPalComponent = ({
};
const createSubscription = async (data, actions) => {
let planId = config.scriptData.subscription_plan_id;
if (config.scriptData.variable_paypal_subscription_variation_from_cart !== '') {
planId = config.scriptData.variable_paypal_subscription_variation_from_cart;
}
return actions.subscription.create({
'plan_id': config.scriptData.subscription_plan_id
'plan_id': planId
});
};
@ -502,6 +508,27 @@ const PayPalComponent = ({
);
}
const BlockEditorPayPalComponent = () => {
const urlParams = {
clientId: 'test',
...config.scriptData.url_params,
dataNamespace: 'ppcp-blocks-editor-paypal-buttons',
components: 'buttons',
}
return (
<PayPalScriptProvider
options={urlParams}
>
<PayPalButtons
onClick={(data, actions) => {
return false;
}}
/>
</PayPalScriptProvider>
)
}
const features = ['products'];
let block_enabled = true;
@ -566,7 +593,7 @@ if (block_enabled) {
name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false}/>,
edit: <PayPalComponent isEditing={true}/>,
edit: <BlockEditorPayPalComponent />,
ariaLabel: config.title,
canMakePayment: () => {
return true;
@ -582,7 +609,7 @@ if (block_enabled) {
paymentMethodId: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent isEditing={false} fundingSource={fundingSource}/>,
edit: <PayPalComponent isEditing={true} fundingSource={fundingSource}/>,
edit: <BlockEditorPayPalComponent />,
ariaLabel: config.title,
canMakePayment: async () => {
if (!paypalScriptPromise) {

View file

@ -111,6 +111,13 @@ class BlocksModule implements ModuleInterface {
}
);
add_filter(
'woocommerce_paypal_payments_sdk_components_hook',
function( array $components ) {
$components[] = 'buttons';
return $components;
}
);
}
/**

View file

@ -1005,6 +1005,28 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@paypal/paypal-js@^8.0.4":
version "8.0.4"
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.0.4.tgz#abe9f40f519b1d2c306adddfbe733be03eb26ce5"
integrity sha512-91g5fhRBHGEBoikDzQT6uBn3PzlJQ75g0c3MvqVJqN0XRm5kHa9wz+6+Uaq8QQuxRzz5C2x55Zg057CW6EuwpQ==
dependencies:
promise-polyfill "^8.3.0"
"@paypal/react-paypal-js@^8.2.0":
version "8.2.0"
resolved "https://registry.yarnpkg.com/@paypal/react-paypal-js/-/react-paypal-js-8.2.0.tgz#4b1a142bbb68e62dca4a92da4a6b5568f54901f0"
integrity sha512-SworUfu0BNNcqoh0O53Ke4MFpx2m3qJRu3hayXvlluEEXJpKqGSV5aaSGFhbsZqi8hnbsx/hZR7BQbmqsggiGQ==
dependencies:
"@paypal/paypal-js" "^8.0.4"
"@paypal/sdk-constants" "^1.0.122"
"@paypal/sdk-constants@^1.0.122":
version "1.0.145"
resolved "https://registry.yarnpkg.com/@paypal/sdk-constants/-/sdk-constants-1.0.145.tgz#f373cd3b7cf0409c28e121629b8f1855faf2e77b"
integrity sha512-2tclrVX3L44YRJ09H4kkqma/UeAjRJarwH5SyPip10O5S+2ByE1HiyspAJ6hIWSjMngFVqjNyUCnrGcuQA3ldg==
dependencies:
hi-base32 "^0.5.0"
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@ -1179,7 +1201,7 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1"
integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==
"@woocommerce/dependency-extraction-webpack-plugin@^2.2.0":
"@woocommerce/dependency-extraction-webpack-plugin@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@woocommerce/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-2.2.0.tgz#230d674a67585bc32e31bc28485bec99b41dbd1f"
integrity sha512-0wDY3EIUwWrPm0KrWvt1cf2SZDSX7CzBXvv4TyCqWOPuVPvC/ajyY8kD1HTFI80q6/RHoxWf3BYCmhuBzPbe9A==
@ -1599,6 +1621,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hi-base32@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e"
integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==
immutable@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
@ -1674,7 +1701,7 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"
js-tokens@^4.0.0:
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@ -1745,6 +1772,13 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@ -1789,6 +1823,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@ -1840,6 +1879,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
promise-polyfill@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@ -1852,6 +1896,23 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
react-dom@^17.0.0:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler "^0.20.2"
react@^17.0.0:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@ -1957,6 +2018,14 @@ sass@^1.42.1:
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"

View file

@ -14,7 +14,7 @@
}
}
.ppc-button-wrapper #ppcp-messages:first-child {
.ppc-button-wrapper .ppcp-messages:first-child {
padding-top: 10px;
}

View file

@ -9,11 +9,11 @@ class CartActionHandler {
this.errorHandler = errorHandler;
}
subscriptionsConfiguration() {
subscriptionsConfiguration(subscription_plan_id) {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
'plan_id': subscription_plan_id
});
},
onApprove: (data, actions) => {

View file

@ -12,7 +12,7 @@ class CheckoutActionHandler {
this.spinner = spinner;
}
subscriptionsConfiguration() {
subscriptionsConfiguration(subscription_plan_id) {
return {
createSubscription: async (data, actions) => {
try {
@ -22,7 +22,7 @@ class CheckoutActionHandler {
}
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
'plan_id': subscription_plan_id
});
},
onApprove: (data, actions) => {

View file

@ -90,7 +90,12 @@ class CartBootstrap {
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration());
let subscription_plan_id = PayPalCommerceGateway.subscription_plan_id
if(PayPalCommerceGateway.variable_paypal_subscription_variation_from_cart !== '') {
subscription_plan_id = PayPalCommerceGateway.variable_paypal_subscription_variation_from_cart
}
this.renderer.render(actionHandler.subscriptionsConfiguration(subscription_plan_id));
if(!PayPalCommerceGateway.subscription_product_allowed) {
this.gateway.button.is_disabled = true;

View file

@ -106,7 +106,11 @@ class CheckoutBootstap {
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration(), {}, actionHandler.configuration());
let subscription_plan_id = PayPalCommerceGateway.subscription_plan_id
if(PayPalCommerceGateway.variable_paypal_subscription_variation_from_cart !== '') {
subscription_plan_id = PayPalCommerceGateway.variable_paypal_subscription_variation_from_cart
}
this.renderer.render(actionHandler.subscriptionsConfiguration(subscription_plan_id), {}, actionHandler.configuration());
if(!PayPalCommerceGateway.subscription_product_allowed) {
this.gateway.button.is_disabled = true;

View file

@ -11,22 +11,20 @@ class MessagesBootstrap {
}
}
init() {
async init() {
if (this.gateway.messages?.block?.enabled) {
this.discoverBlocks();
await this.attemptDiscoverBlocks(3); // Try up to 3 times
}
jQuery(document.body).on('ppcp_cart_rendered ppcp_checkout_rendered', () => {
this.render();
});
jQuery(document.body).on('ppcp_script_data_changed', (e, data) => {
this.gateway = data;
this.render();
});
jQuery(document.body).on('ppcp_cart_total_updated ppcp_checkout_total_updated ppcp_product_total_updated ppcp_block_cart_total_updated', (e, amount) => {
if (this.lastAmount !== amount) {
this.lastAmount = amount;
this.render();
}
});
@ -34,13 +32,39 @@ class MessagesBootstrap {
this.render();
}
attemptDiscoverBlocks(retries) {
return new Promise((resolve, reject) => {
this.discoverBlocks().then(found => {
if (!found && retries > 0) {
setTimeout(() => {
this.attemptDiscoverBlocks(retries - 1).then(resolve);
}, 2000); // Wait 2 seconds before retrying
} else {
resolve();
}
});
});
}
discoverBlocks() {
Array.from(document.querySelectorAll('.ppcp-paylater-message-block')).forEach(blockElement => {
const config = {wrapper: '#' + blockElement.id};
if (!blockElement.getAttribute('data-pp-placement')) {
config.placement = this.gateway.messages.placement;
return new Promise((resolve) => {
const elements = document.querySelectorAll('.ppcp-messages');
if (elements.length === 0) {
resolve(false);
return;
}
this.renderers.push(new MessageRenderer(config));
Array.from(elements).forEach(blockElement => {
if (!blockElement.id) {
blockElement.id = `ppcp-message-${Math.random().toString(36).substr(2, 9)}`; // Ensure each block has a unique ID
}
const config = {wrapper: '#' + blockElement.id};
if (!blockElement.getAttribute('data-pp-placement')) {
config.placement = this.gateway.messages.placement;
}
this.renderers.push(new MessageRenderer(config));
});
resolve(true);
});
}

View file

@ -3,6 +3,7 @@ import {loadScript} from "@paypal/paypal-js";
import widgetBuilder from "../Renderer/WidgetBuilder";
import merge from "deepmerge";
import {keysToCamelCase} from "./Utils";
import {getCurrentPaymentMethod} from "./CheckoutMethodState";
// This component may be used by multiple modules. This assures that options are shared between all instances.
let options = window.ppcpWidgetBuilder = window.ppcpWidgetBuilder || {
@ -60,6 +61,13 @@ export const loadPaypalScript = (config, onLoaded, onError = null) => {
scriptOptions = merge(scriptOptions, config.script_attributes);
}
// Axo SDK options
const sdkClientToken = config?.axo?.sdk_client_token;
if(sdkClientToken) {
scriptOptions['data-sdk-client-token'] = sdkClientToken;
scriptOptions['data-client-metadata-id'] = 'ppcp-cm-id';
}
// Load PayPal script for special case with data-client-token
if (config.data_client_id?.set_attribute) {
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback);
@ -68,7 +76,7 @@ export const loadPaypalScript = (config, onLoaded, onError = null) => {
// Adds data-user-id-token to script options.
const userIdToken = config?.save_payment_methods?.id_token;
if(userIdToken) {
if(userIdToken && !sdkClientToken) {
scriptOptions['data-user-id-token'] = userIdToken;
}

View file

@ -34,6 +34,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule;
use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksModule;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
@ -438,7 +439,20 @@ class SmartButton implements SmartButtonInterface {
wp_is_block_theme()
);
$get_hook = function ( string $location ) use ( $default_pay_order_hook, $is_block_theme ): ?array {
$has_paylater_block =
(
PayLaterBlockModule::is_block_enabled( $this->settings_status ) &&
has_block( 'woocommerce-paypal-payments/paylater-messages' )
) ||
(
PayLaterWCBlocksModule::is_block_enabled( $this->settings_status, $location ) &&
(
has_block( 'woocommerce-paypal-payments/checkout-paylater-messages' ) ||
has_block( 'woocommerce-paypal-payments/cart-paylater-messages' )
)
);
$get_hook = function ( string $location ) use ( $default_pay_order_hook, $is_block_theme, $has_paylater_block ): ?array {
switch ( $location ) {
case 'checkout':
return $this->messages_renderer_hook( $location, 'woocommerce_review_order_before_payment', 10 );
@ -457,11 +471,14 @@ class SmartButton implements SmartButtonInterface {
? $this->messages_renderer_block( $location, 'core/navigation', 10 )
: $this->messages_renderer_hook( $location, 'loop_start', 20 );
default:
return null;
return $has_paylater_block
? $this->messages_renderer_hook( $location, 'ppcp_paylater_message_block', 10 )
: null;
}
};
$hook = $get_hook( $location );
if ( ! $hook ) {
return false;
}
@ -492,7 +509,7 @@ class SmartButton implements SmartButtonInterface {
function () {
echo '
<script>
document.querySelector("#payment").before(document.querySelector("#ppcp-messages"))
document.querySelector("#payment").before(document.querySelector(".ppcp-messages"))
</script>';
}
);
@ -601,10 +618,6 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return false;
}
if ( in_array( $this->context(), array( 'checkout-block', 'cart-block' ), true ) ) {
return false;
}
return $this->should_load_buttons() || $this->should_load_messages() || $this->can_render_dcc();
}
@ -655,7 +668,11 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
$has_paylater_block = has_block( 'woocommerce-paypal-payments/paylater-messages' ) && PayLaterBlockModule::is_block_enabled( $this->settings_status );
$has_paylater_block = PayLaterBlockModule::is_block_enabled( $this->settings_status ) && has_block( 'woocommerce-paypal-payments/paylater-messages' );
if ( 'cart-block' === $location || 'checkout-block' === $location ) {
return true;
}
switch ( $location ) {
case 'checkout':
@ -667,9 +684,6 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
return $messaging_enabled_for_current_location;
case 'block-editor':
return true;
case 'checkout-block':
case 'cart-block':
return $has_paylater_block || $this->is_block_editor();
default:
return $has_paylater_block;
}
@ -789,7 +803,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
*/
do_action( "ppcp_before_{$location_hook}_message_wrapper" );
$messages_placeholder = '<div id="ppcp-messages" data-partner-attribution-id="Woo_PPCP"></div>';
$messages_placeholder = '<div class="ppcp-messages" data-partner-attribution-id="Woo_PPCP"></div>';
if ( is_array( $block_params ) && ( $block_params['blockName'] ?? false ) ) {
$this->render_after_block(
@ -888,7 +902,20 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$styling_per_location = $this->settings->has( 'pay_later_enable_styling_per_messaging_location' ) && $this->settings->get( 'pay_later_enable_styling_per_messaging_location' );
$location = $styling_per_location ? $location : 'general';
$setting_name_prefix = "pay_later_{$location}_message";
// Map checkout-block and cart-block message options to checkout and cart options.
switch ( $location ) {
case 'checkout-block':
$location = 'checkout';
break;
case 'cart-block':
$location = 'cart';
break;
default:
break;
}
$setting_name_prefix = "pay_later_{$location}_message";
$layout = $this->settings->has( "{$setting_name_prefix}_layout" ) ? $this->settings->get( "{$setting_name_prefix}_layout" ) : 'text';
$logo_type = $this->settings->has( "{$setting_name_prefix}_logo" ) ? $this->settings->get( "{$setting_name_prefix}_logo" ) : 'primary';
@ -899,7 +926,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
$text_size = $this->settings->has( "{$setting_name_prefix}_text_size" ) ? $this->settings->get( "{$setting_name_prefix}_text_size" ) : '12';
return array(
'wrapper' => '#ppcp-messages',
'wrapper' => '.ppcp-messages',
'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ),
'block' => array(
'enabled' => PayLaterBlockModule::is_block_enabled( $this->settings_status ),
@ -1111,6 +1138,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(),
'vault_v3_enabled' => $this->vault_v3_enabled,
'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(),
'variable_paypal_subscription_variation_from_cart' => $this->subscription_helper->paypal_subscription_variation_from_cart(),
'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(),
'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(),
'enforce_vault' => $this->has_subscriptions(),
@ -1428,9 +1456,45 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
* @return array
*/
private function attributes(): array {
return array(
$attributes = array(
'data-partner-attribution-id' => $this->bn_code_for_context( $this->context() ),
);
$page_type_attribute = $this->page_type_attribute();
if ( $page_type_attribute ) {
$attributes['data-page-type'] = $page_type_attribute;
}
return $attributes;
}
/**
* Retrieves the value for page type attribute(data-page-type) used for the JS SDK.
*
* @return string
*/
protected function page_type_attribute(): string {
if ( is_search() ) {
return 'search-results';
}
switch ( $this->location() ) {
case 'product':
return 'product-details';
case 'shop':
return 'product-listing';
case 'home':
return 'mini-cart';
case 'cart-block':
case 'cart':
return 'cart';
case 'checkout-block':
case 'checkout':
case 'pay-now':
return 'checkout';
default:
return '';
}
}
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -39,6 +39,8 @@ return array(
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
$module_url = $container->get( 'googlepay.url' );
// Connection tab fields.
$fields = $insert_after(
$fields,
@ -62,10 +64,15 @@ return array(
$connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">';
return $insert_after(
$fields,
'allow_card_button_gateway',
'digital_wallet_heading',
array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/googlepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Google Pay', 'woocommerce-paypal-payments' )
),
'type' => 'checkbox',
'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => array( 'ppcp-disabled-checkbox' ),
@ -80,7 +87,7 @@ return array(
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
@ -93,6 +100,7 @@ return array(
)
),
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
)
);
@ -101,10 +109,15 @@ return array(
// Standard Payments tab fields.
return $insert_after(
$fields,
'allow_card_button_gateway',
'digital_wallet_heading',
array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/googlepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Google Pay', 'woocommerce-paypal-payments' )
),
'type' => 'checkbox',
'label' => __( 'Enable Google Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">'
@ -117,7 +130,7 @@ return array(
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
@ -129,10 +142,12 @@ return array(
->action_visible( 'googlepay_button_color' )
->action_visible( 'googlepay_button_language' )
->action_visible( 'googlepay_button_shipping_enabled' )
->action_class( 'googlepay_button_enabled', 'active' )
->to_array(),
)
),
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
'googlepay_button_type' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
@ -148,7 +163,7 @@ return array(
'default' => 'pay',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'googlepay_button_color' => array(
@ -166,7 +181,7 @@ return array(
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'googlepay_button_language' => array(
@ -183,7 +198,7 @@ return array(
'default' => 'en',
'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
'googlepay_button_shipping_enabled' => array(
@ -198,7 +213,7 @@ return array(
'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ),
'default' => 'no',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'gateway' => 'dcc',
'requirements' => array(),
),
)

View file

@ -28,7 +28,7 @@ class GooglepayButton {
this.log = function() {
if ( this.buttonConfig.is_debug ) {
console.log('[GooglePayButton]', ...arguments);
//console.log('[GooglePayButton]', ...arguments);
}
}
}

View file

@ -136,7 +136,10 @@ class OnboardingOptionsRenderer {
__( 'For Standard payments, Casual sellers may connect their Personal PayPal account in eligible countries to sell on WooCommerce. For Advanced payments, a Business PayPal account is required.', 'woocommerce-paypal-payments' )
),
);
$items[] = '
$basic_table_rows = apply_filters( 'ppcp_onboarding_basic_table_rows', $basic_table_rows );
$items[] = '
<li>
<label>
<input type="radio" id="ppcp-onboarding-dcc-basic" name="ppcp_onboarding_dcc" value="basic" checked ' .
@ -191,7 +194,10 @@ class OnboardingOptionsRenderer {
__( 'For Standard payments, Casual sellers may connect their Personal PayPal account in eligible countries to sell on WooCommerce. For Advanced payments, a Business PayPal account is required.', 'woocommerce-paypal-payments' )
),
);
$items[] = '
$dcc_table_rows = apply_filters( 'ppcp_onboarding_dcc_table_rows', $dcc_table_rows, $this );
$items[] = '
<li>
<label>
<input type="radio" id="ppcp-onboarding-dcc-acdc" name="ppcp_onboarding_dcc" value="acdc" ' .
@ -224,7 +230,7 @@ class OnboardingOptionsRenderer {
* @param string $note The additional description text, such as about conditions.
* @return string
*/
private function render_table_row( string $header, string $value, string $tooltip = '', string $note = '' ): string {
public function render_table_row( string $header, string $value, string $tooltip = '', string $note = '' ): string {
$value_html = $value;
if ( $note ) {
$value_html .= '<br/><span class="ppcp-muted-text">' . $note . '</span>';

View file

@ -7,40 +7,45 @@
"description": "PayPal Pay Later messaging will be displayed for eligible customers. Customers automatically see the most relevant Pay Later offering.",
"example": {},
"attributes": {
"id": {
"type": "string"
},
"layout": {
"type": "string",
"default": "flex"
},
"logo": {
"type": "string",
"default": "inline"
},
"position": {
"type": "string",
"default": "left"
},
"color": {
"type": "string",
"default": "black"
},
"flexColor": {
"type": "string",
"default": "white"
},
"flexRatio": {
"type": "string",
"default": "8x1"
},
"placement": {
"type": "string",
"default": "auto"
}
"id": {
"type": "string"
},
"layout": {
"type": "string",
"default": "flex"
},
"logo": {
"type": "string",
"default": "inline"
},
"position": {
"type": "string",
"default": "left"
},
"color": {
"type": "string",
"default": "black"
},
"size": {
"type": "string",
"default": "14"
},
"flexColor": {
"type": "string",
"default": "white"
},
"flexRatio": {
"type": "string",
"default": "8x1"
},
"placement": {
"type": "string",
"default": "auto"
}
},
"supports": {
"html": false
"html": false,
"multiple": false
},
"textdomain": "woocommerce-paypal-payments",
"editorScript": "ppcp-paylater-block",

156
modules/ppcp-paylater-block/composer.lock generated Normal file
View file

@ -0,0 +1,156 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "51e234e39d0fd6bbb67fe2186683facd",
"packages": [
{
"name": "container-interop/service-provider",
"version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/container-interop/service-provider.git",
"reference": "e04441ca21ef03e10dce70b0af29269281eec6dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/container-interop/service-provider/zipball/e04441ca21ef03e10dce70b0af29269281eec6dc",
"reference": "e04441ca21ef03e10dce70b0af29269281eec6dc",
"shasum": ""
},
"require": {
"psr/container": "^1.0 || ^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Interop\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Promoting container interoperability through standard service providers",
"homepage": "https://github.com/container-interop/service-provider",
"support": {
"issues": "https://github.com/container-interop/service-provider/issues",
"source": "https://github.com/container-interop/service-provider/tree/v0.4.1"
},
"time": "2023-12-14T14:50:12+00:00"
},
{
"name": "dhii/module-interface",
"version": "v0.3.0-alpha2",
"source": {
"type": "git",
"url": "https://github.com/Dhii/module-interface.git",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dhii/module-interface/zipball/0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"shasum": ""
},
"require": {
"container-interop/service-provider": "^0.4",
"php": "^7.1 | ^8.0",
"psr/container": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 | ^8.0 | ^9.0",
"slevomat/coding-standard": "^6.0",
"vimeo/psalm": "^3.11.7 | ^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-develop": "0.3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dhii\\Modular\\Module\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dhii Team",
"email": "development@dhii.co"
}
],
"description": "Interfaces for modules",
"support": {
"issues": "https://github.com/Dhii/module-interface/issues",
"source": "https://github.com/Dhii/module-interface/tree/v0.3.0-alpha2"
},
"time": "2021-08-23T08:23:01+00:00"
},
{
"name": "psr/container",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.2"
},
"time": "2021-11-05T16:50:12+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^7.2 | ^8.0"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -10,13 +10,16 @@
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0"
"@paypal/react-paypal-js": "^8.2.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",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",

View file

@ -1,29 +0,0 @@
import { useRef, useEffect } from '@wordpress/element';
export default function PayPalMessages({
amount,
style,
onRender,
}) {
const containerRef = useRef(null);
useEffect(() => {
const messages = paypal.Messages({
amount,
style,
onRender,
});
messages.render(containerRef.current)
.catch(err => {
// Ignore when component destroyed.
if (!containerRef.current || containerRef.current.children.length === 0) {
return;
}
console.error(err);
});
}, [amount, style, onRender]);
return <div ref={containerRef}/>
}

View file

@ -2,19 +2,16 @@ import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, SelectControl, Spinner } from '@wordpress/components';
import { useScriptParams } from "./hooks/script-params";
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
import PayPalMessages from "./components/PayPalMessages";
import { PayPalScriptProvider, PayPalMessages } from '@paypal/react-paypal-js';
import { useScriptParams } from './hooks/script-params';
export default function Edit( { attributes, clientId, setAttributes } ) {
export default function Edit({ attributes, clientId, setAttributes }) {
const { layout, logo, position, color, size, flexColor, flexRatio, placement, id } = attributes;
const isFlex = layout === 'flex';
const [paypalScriptState, setPaypalScriptState] = useState(null);
const [loaded, setLoaded] = useState(false);
const [rendered, setRendered] = useState(false);
let amount = undefined;
let amount;
const postContent = String(wp.data.select('core/editor')?.getEditedPostContent());
if (postContent.includes('woocommerce/checkout') || postContent.includes('woocommerce/cart')) {
amount = 50.0;
@ -36,159 +33,171 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
let classes = ['ppcp-paylater-block-preview', 'ppcp-overlay-parent'];
if (PcpPayLaterBlock.vaultingEnabled || !PcpPayLaterBlock.placementEnabled) {
classes = ['ppcp-paylater-block-preview', 'ppcp-paylater-unavailable', 'block-editor-warning'];
classes.push('ppcp-paylater-unavailable', 'block-editor-warning');
}
const props = useBlockProps({className: classes});
const loadingElement = <div {...props}><Spinner/></div>;
const props = useBlockProps({ className: classes.join(' ') });
useEffect(() => {
if (!id) {
setAttributes({id: 'ppcp-' + clientId});
setAttributes({ id: `ppcp-${clientId}` });
}
}, []);
}, [id, clientId]);
if (PcpPayLaterBlock.vaultingEnabled) {
return <div {...props}>
<div className={'block-editor-warning__contents'}>
<h3>{__('PayPal Pay Later Messaging', 'woocommerce-paypal-payments')}</h3>
<p className={'block-editor-warning__message'}>{__('Pay Later Messaging cannot be used while PayPal Vaulting is active. Disable PayPal Vaulting in the PayPal Payment settings to reactivate this block', 'woocommerce-paypal-payments')}</p>
<div className={'class="block-editor-warning__actions"'}>
<span className={'block-editor-warning__action'}>
<a href={PcpPayLaterBlock.settingsUrl} className={'components-button is-primary'}>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</a>
</span>
<span className={'block-editor-warning__action'}>
<button onClick={() => wp.data.dispatch( 'core/block-editor' ).removeBlock(clientId)} type={'button'} className={'components-button is-secondary'}>
{__('Remove Block', 'woocommerce-paypal-payments')}
</button>
</span>
return (
<div {...props}>
<div className='block-editor-warning__contents'>
<p className='block-editor-warning__message'>
{__('Pay Later Messaging cannot be used while PayPal Vaulting is active. Disable PayPal Vaulting in the PayPal Payment settings to reactivate this block', 'woocommerce-paypal-payments')}
</p>
<div className='block-editor-warning__actions'>
<span className='block-editor-warning__action'>
<a href={PcpPayLaterBlock.payLaterSettingsUrl}>
<button type='button' className='components-button is-primary'>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</span>
<span className='block-editor-warning__action'>
<button
onClick={() => wp.data.dispatch('core/block-editor').removeBlock(clientId)}
type='button' className='components-button is-secondary'>
{__('Remove Block', 'woocommerce-paypal-payments')}
</button>
</span>
</div>
</div>
</div>
</div>
);
}
if (!PcpPayLaterBlock.placementEnabled) {
return <div {...props}>
<div className={'block-editor-warning__contents'}>
<h3>{__('PayPal Pay Later Messaging', 'woocommerce-paypal-payments')}</h3>
<p className={'block-editor-warning__message'}>{__('Pay Later Messaging cannot be used while the “WooCommerce Block” messaging placement is disabled. Enable the placement in the PayPal Payments Pay Later settings to reactivate this block.', 'woocommerce-paypal-payments')}</p>
<div className={'class="block-editor-warning__actions"'}>
<span className={'block-editor-warning__action'}>
<a href={PcpPayLaterBlock.payLaterSettingsUrl} className={'components-button is-primary'}>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</a>
</span>
<span className={'block-editor-warning__action'}>
<button onClick={() => wp.data.dispatch( 'core/block-editor' ).removeBlock(clientId)} type={'button'} className={'components-button is-secondary'}>
{__('Remove Block', 'woocommerce-paypal-payments')}
</button>
</span>
return (
<div {...props}>
<div className='block-editor-warning__contents'>
<p className='block-editor-warning__message'>
{__('Pay Later Messaging cannot be used while the “WooCommerce Block” messaging placement is disabled. Enable the placement in the PayPal Payments Pay Later settings to reactivate this block.', 'woocommerce-paypal-payments')}
</p>
<div className='block-editor-warning__actions'>
<span className='block-editor-warning__action'>
<a href={PcpPayLaterBlock.payLaterSettingsUrl}>
<button type='button' className='components-button is-primary'>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</span>
<span className='block-editor-warning__action'>
<button
onClick={() => wp.data.dispatch('core/block-editor').removeBlock(clientId)}
type='button' className='components-button is-secondary'>
{__('Remove Block', 'woocommerce-paypal-payments')}
</button>
</span>
</div>
</div>
</div>
</div>
);
}
let scriptParams = useScriptParams(PcpPayLaterBlock.ajax.cart_script_params);
const scriptParams = useScriptParams(PcpPayLaterBlock.ajax.cart_script_params);
if (scriptParams === null) {
return loadingElement;
}
if (scriptParams === false) {
scriptParams = {
url_params: {
clientId: 'test',
}
}
}
scriptParams.url_params.components = 'messages,buttons,funding-eligibility';
if (!paypalScriptState) {
loadPaypalScript(scriptParams, () => {
setPaypalScriptState('loaded')
}, () => {
setPaypalScriptState('failed')
});
}
if (paypalScriptState !== 'loaded') {
return loadingElement;
return <div {...props}><Spinner/></div>;
}
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Settings', 'woocommerce-paypal-payments' ) }>
const urlParams = {
...scriptParams.url_params,
components: 'messages',
dataNamespace: 'ppcp-block-editor-paylater-message',
};
return (
<>
<InspectorControls>
<PanelBody title={__('Settings', 'woocommerce-paypal-payments')}>
<SelectControl
label={ __( 'Layout', 'woocommerce-paypal-payments' ) }
options={ [
{ label: __( 'Text', 'woocommerce-paypal-payments' ), value: 'text' },
{ label: __( 'Banner', 'woocommerce-paypal-payments' ), value: 'flex' },
] }
value={ layout }
onChange={ ( value ) => setAttributes( { layout: value } ) }
label={__('Layout', 'woocommerce-paypal-payments')}
options={[
{ label: __('Text', 'woocommerce-paypal-payments'), value: 'text' },
{ label: __('Banner', 'woocommerce-paypal-payments'), value: 'flex' },
]}
value={layout}
onChange={(value) => setAttributes({ layout: value })}
/>
{ !isFlex && (<SelectControl
label={__('Logo', 'woocommerce-paypal-payments')}
options={[
{ label: __('Full logo', 'woocommerce-paypal-payments'), value: 'primary' },
{ label: __('Monogram', 'woocommerce-paypal-payments'), value: 'alternative' },
{ label: __('Inline', 'woocommerce-paypal-payments'), value: 'inline' },
{ label: __('Message only', 'woocommerce-paypal-payments'), value: 'none' },
]}
value={logo}
onChange={(value) => setAttributes({logo: value})}
/>)}
{ !isFlex && logo === 'primary' && (<SelectControl
label={__('Logo Position', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Left', 'woocommerce-paypal-payments' ), value: 'left' },
{ label: __( 'Right', 'woocommerce-paypal-payments' ), value: 'right' },
{ label: __( 'Top', 'woocommerce-paypal-payments' ), value: 'top' },
]}
value={position}
onChange={(value) => setAttributes({position: value})}
/>)}
{ !isFlex && (<SelectControl
label={__('Text Color', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Black / Blue logo', 'woocommerce-paypal-payments' ), value: 'black' },
{ label: __( 'White / White logo', 'woocommerce-paypal-payments' ), value: 'white' },
{ label: __( 'Monochrome', 'woocommerce-paypal-payments' ), value: 'monochrome' },
{ label: __( 'Black / Gray logo', 'woocommerce-paypal-payments' ), value: 'grayscale' },
]}
value={color}
onChange={(value) => setAttributes({color: value})}
/>)}
{ !isFlex && (<SelectControl
label={__('Text Size', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Small', 'woocommerce-paypal-payments' ), value: '12' },
{ label: __( 'Medium', 'woocommerce-paypal-payments' ), value: '14' },
{ label: __( 'Large', 'woocommerce-paypal-payments' ), value: '16' },
]}
value={size}
onChange={(value) => setAttributes({size: value})}
/>)}
{ isFlex && (<SelectControl
label={__('Color', 'woocommerce-paypal-payments')}
options={[
{ label: __( 'Blue', 'woocommerce-paypal-payments' ), value: 'blue' },
{ label: __( 'Black', 'woocommerce-paypal-payments' ), value: 'black' },
{ label: __( 'White', 'woocommerce-paypal-payments' ), value: 'white' },
{ label: __( 'White (no border)', 'woocommerce-paypal-payments' ), value: 'white-no-border' },
]}
value={flexColor}
onChange={(value) => setAttributes({flexColor: value})}
/>)}
{ isFlex && (<SelectControl
label={__('Ratio', 'woocommerce-paypal-payments')}
options={[
{ label: __( '8x1', 'woocommerce-paypal-payments' ), value: '8x1' },
{ label: __( '20x1', 'woocommerce-paypal-payments' ), value: '20x1' },
]}
value={flexRatio}
onChange={(value) => setAttributes({flexRatio: value})}
/>)}
{!isFlex && (
<SelectControl
label={__('Logo', 'woocommerce-paypal-payments')}
options={[
{ label: __('Full logo', 'woocommerce-paypal-payments'), value: 'primary' },
{ label: __('Monogram', 'woocommerce-paypal-payments'), value: 'alternative' },
{ label: __('Inline', 'woocommerce-paypal-payments'), value: 'inline' },
{ label: __('Message only', 'woocommerce-paypal-payments'), value: 'none' },
]}
value={logo}
onChange={(value) => setAttributes({ logo: value })}
/>
)}
{!isFlex && logo === 'primary' && (
<SelectControl
label={__('Logo Position', 'woocommerce-paypal-payments')}
options={[
{ label: __('Left', 'woocommerce-paypal-payments'), value: 'left' },
{ label: __('Right', 'woocommerce-paypal-payments'), value: 'right' },
{ label: __('Top', 'woocommerce-paypal-payments'), value: 'top' },
]}
value={position}
onChange={(value) => setAttributes({ position: value })}
/>
)}
{!isFlex && (
<SelectControl
label={__('Text Color', 'woocommerce-paypal-payments')}
options={[
{ label: __('Black / Blue logo', 'woocommerce-paypal-payments'), value: 'black' },
{ label: __('White / White logo', 'woocommerce-paypal-payments'), value: 'white' },
{ label: __('Monochrome', 'woocommerce-paypal-payments'), value: 'monochrome' },
{ label: __('Black / Gray logo', 'woocommerce-paypal-payments'), value: 'grayscale' },
]}
value={color}
onChange={(value) => setAttributes({ color: value })}
/>
)}
{!isFlex && (
<SelectControl
label={__('Text Size', 'woocommerce-paypal-payments')}
options={[
{ label: __('Small', 'woocommerce-paypal-payments'), value: '12' },
{ label: __('Medium', 'woocommerce-paypal-payments'), value: '14' },
{ label: __('Large', 'woocommerce-paypal-payments'), value: '16' },
]}
value={size}
onChange={(value) => setAttributes({ size: value })}
/>
)}
{isFlex && (
<SelectControl
label={__('Color', 'woocommerce-paypal-payments')}
options={[
{ label: __('Blue', 'woocommerce-paypal-payments'), value: 'blue' },
{ label: __('Black', 'woocommerce-paypal-payments'), value: 'black' },
{ label: __('White', 'woocommerce-paypal-payments'), value: 'white' },
{ label: __('White (no border)', 'woocommerce-paypal-payments'), value: 'white-no-border' },
]}
value={flexColor}
onChange={(value) => setAttributes({ flexColor: value })}
/>
)}
{isFlex && (
<SelectControl
label={__('Ratio', 'woocommerce-paypal-payments')}
options={[
{ label: __('8x1', 'woocommerce-paypal-payments'), value: '8x1' },
{ label: __('20x1', 'woocommerce-paypal-payments'), value: '20x1' },
]}
value={flexRatio}
onChange={(value) => setAttributes({ flexRatio: value })}
/>
)}
<SelectControl
label={ __( 'Placement page', 'woocommerce-paypal-payments' ) }
help={ __( 'Used for the analytics dashboard in the merchant account.', 'woocommerce-paypal-payments' ) }
@ -203,20 +212,23 @@ export default function Edit( { attributes, clientId, setAttributes } ) {
value={ placement }
onChange={ ( value ) => setAttributes( { placement: value } ) }
/>
</PanelBody>
</InspectorControls>
</PanelBody>
</InspectorControls>
<div {...props}>
<div className={'ppcp-overlay-child'}>
<PayPalMessages
style={previewStyle}
amount={amount}
onRender={() => setRendered(true)}
/>
<div className='ppcp-overlay-child'>
<PayPalScriptProvider options={urlParams}>
<PayPalMessages
style={previewStyle}
forceReRender={[previewStyle]}
onRender={() => setLoaded(true)}
amount={amount}
/>
</PayPalScriptProvider>
</div>
<div className={'ppcp-overlay-child ppcp-unclicable-overlay'}> {/* make the message not clickable */}
{!rendered && (<Spinner/>)}
<div className='ppcp-overlay-child ppcp-unclicable-overlay'> {/* make the message not clickable */}
{!loaded && <Spinner />}
</div>
</div>
</>
);
</>
);
}

View file

@ -1,7 +1,6 @@
import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import save from './save';
const paypalIcon = (
<svg width="584.798" height="720" viewBox="0 0 154.728 190.5">
@ -18,7 +17,9 @@ const blockId = 'woocommerce-paypal-payments/paylater-messages';
registerBlockType( blockId, {
icon: paypalIcon,
edit: Edit,
save,
save() {
return null;
},
} );
document.addEventListener( 'DOMContentLoaded', () => {

View file

@ -1,26 +0,0 @@
import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { layout, logo, position, color, size, flexColor, flexRatio, placement, id } = attributes;
const paypalAttributes = layout === 'flex' ? {
'data-pp-style-layout': 'flex',
'data-pp-style-color': flexColor,
'data-pp-style-ratio': flexRatio,
} : {
'data-pp-style-layout': 'text',
'data-pp-style-logo-type': logo,
'data-pp-style-logo-position': position,
'data-pp-style-text-color': color,
'data-pp-style-text-size': size,
};
if (placement && placement !== 'auto') {
paypalAttributes['data-pp-placement'] = placement;
}
const props = {
className: 'ppcp-paylater-message-block',
id,
...paypalAttributes,
};
return <div { ...useBlockProps.save(props) }></div>;
}

View file

@ -9,10 +9,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterBlock;
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockRenderer;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'paylater-block.url' => static function ( ContainerInterface $container ): string {
'paylater-block.url' => static function ( ContainerInterface $container ): string {
/**
* Cannot return false for this path.
*
@ -23,4 +24,7 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'paylater-block.renderer' => static function (): PayLaterBlockRenderer {
return new PayLaterBlockRenderer();
},
);

View file

@ -99,7 +99,23 @@ class PayLaterBlockModule implements ModuleInterface {
*
* @psalm-suppress PossiblyFalseArgument
*/
register_block_type( dirname( realpath( __FILE__ ), 2 ) );
register_block_type(
dirname( realpath( __FILE__ ), 2 ),
array(
'render_callback' => function ( array $attributes ) use ( $c ) {
$renderer = $c->get( 'paylater-block.renderer' );
ob_start();
// phpcs:ignore -- No need to escape it, the PayLaterBlockRenderer class is responsible for escaping.
echo $renderer->render(
// phpcs:ignore
$attributes,
// phpcs:ignore
$c
);
return ob_get_clean();
},
)
);
},
20
);

View file

@ -0,0 +1,65 @@
<?php
/**
* Defines the PayLaterBlockRenderer class.
*
* This file is responsible for rendering the Pay Later Messaging block.
*
* @package WooCommerce\PayPalCommerce\PayLaterBlock
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterBlock;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class PayLaterBlockRenderer
*/
class PayLaterBlockRenderer {
/**
* Renders the Pay Later Messaging block.
*
* @param array $attributes The block attributes.
* @param ContainerInterface $c The container.
* @return string The rendered HTML.
*/
public function render( array $attributes, ContainerInterface $c ): string {
if ( PayLaterBlockModule::is_block_enabled( $c->get( 'wcgateway.settings.status' ) ) ) {
$html = '<div id="' . esc_attr( $attributes['id'] ?? '' ) . '" class="ppcp-messages" data-partner-attribution-id="Woo_PPCP"></div>';
$processor = new \WP_HTML_Tag_Processor( $html );
if ( $processor->next_tag( 'div' ) ) {
$layout = $attributes['layout'] ?? 'text';
if ( 'flex' === $layout ) {
$processor->set_attribute( 'data-pp-style-layout', 'flex' );
$processor->set_attribute( 'data-pp-style-color', esc_attr( $attributes['flexColor'] ?? '' ) );
$processor->set_attribute( 'data-pp-style-ratio', esc_attr( $attributes['flexRatio'] ?? '' ) );
} else {
$processor->set_attribute( 'data-pp-style-layout', 'text' );
$processor->set_attribute( 'data-pp-style-logo-type', esc_attr( $attributes['logo'] ?? '' ) );
$processor->set_attribute( 'data-pp-style-logo-position', esc_attr( $attributes['position'] ?? '' ) );
$processor->set_attribute( 'data-pp-style-text-color', esc_attr( $attributes['color'] ?? '' ) );
$processor->set_attribute( 'data-pp-style-text-size', esc_attr( $attributes['size'] ?? '' ) );
}
if ( ( $attributes['placement'] ?? 'auto' ) !== 'auto' ) {
$processor->set_attribute( 'data-pp-placement', esc_attr( $attributes['placement'] ) );
}
}
$updated_html = $processor->get_updated_html();
return sprintf(
'<div id="ppcp-paylater-message-block" %1$s>%2$s</div>',
wp_kses_data( get_block_wrapper_attributes() ),
$updated_html
);
}
return '';
}
}

View file

@ -1005,6 +1005,28 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@paypal/paypal-js@^8.0.4":
version "8.0.4"
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.0.4.tgz#abe9f40f519b1d2c306adddfbe733be03eb26ce5"
integrity sha512-91g5fhRBHGEBoikDzQT6uBn3PzlJQ75g0c3MvqVJqN0XRm5kHa9wz+6+Uaq8QQuxRzz5C2x55Zg057CW6EuwpQ==
dependencies:
promise-polyfill "^8.3.0"
"@paypal/react-paypal-js@^8.2.0":
version "8.2.0"
resolved "https://registry.yarnpkg.com/@paypal/react-paypal-js/-/react-paypal-js-8.2.0.tgz#4b1a142bbb68e62dca4a92da4a6b5568f54901f0"
integrity sha512-SworUfu0BNNcqoh0O53Ke4MFpx2m3qJRu3hayXvlluEEXJpKqGSV5aaSGFhbsZqi8hnbsx/hZR7BQbmqsggiGQ==
dependencies:
"@paypal/paypal-js" "^8.0.4"
"@paypal/sdk-constants" "^1.0.122"
"@paypal/sdk-constants@^1.0.122":
version "1.0.145"
resolved "https://registry.yarnpkg.com/@paypal/sdk-constants/-/sdk-constants-1.0.145.tgz#f373cd3b7cf0409c28e121629b8f1855faf2e77b"
integrity sha512-2tclrVX3L44YRJ09H4kkqma/UeAjRJarwH5SyPip10O5S+2ByE1HiyspAJ6hIWSjMngFVqjNyUCnrGcuQA3ldg==
dependencies:
hi-base32 "^0.5.0"
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@ -1179,7 +1201,7 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1"
integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==
"@woocommerce/dependency-extraction-webpack-plugin@^2.2.0":
"@woocommerce/dependency-extraction-webpack-plugin@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@woocommerce/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-2.2.0.tgz#230d674a67585bc32e31bc28485bec99b41dbd1f"
integrity sha512-0wDY3EIUwWrPm0KrWvt1cf2SZDSX7CzBXvv4TyCqWOPuVPvC/ajyY8kD1HTFI80q6/RHoxWf3BYCmhuBzPbe9A==
@ -1599,6 +1621,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hi-base32@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e"
integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==
immutable@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
@ -1674,7 +1701,7 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"
js-tokens@^4.0.0:
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@ -1745,6 +1772,13 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@ -1789,6 +1823,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@ -1840,6 +1879,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
promise-polyfill@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@ -1852,6 +1896,23 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
react-dom@^17.0.0:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler "^0.20.2"
react@^17.0.0:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@ -1957,6 +2018,14 @@ sass@^1.42.1:
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"

View file

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

View file

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

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-paylater-wc-blocks",
"type": "dhii-mod",
"description": "Pay Later WooCommerce Blocks 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\\PayLaterWCBlocks\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,156 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bbff959c846d515b5cfe39e6d175972b",
"packages": [
{
"name": "container-interop/service-provider",
"version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/container-interop/service-provider.git",
"reference": "e04441ca21ef03e10dce70b0af29269281eec6dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/container-interop/service-provider/zipball/e04441ca21ef03e10dce70b0af29269281eec6dc",
"reference": "e04441ca21ef03e10dce70b0af29269281eec6dc",
"shasum": ""
},
"require": {
"psr/container": "^1.0 || ^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Interop\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Promoting container interoperability through standard service providers",
"homepage": "https://github.com/container-interop/service-provider",
"support": {
"issues": "https://github.com/container-interop/service-provider/issues",
"source": "https://github.com/container-interop/service-provider/tree/v0.4.1"
},
"time": "2023-12-14T14:50:12+00:00"
},
{
"name": "dhii/module-interface",
"version": "v0.3.0-alpha2",
"source": {
"type": "git",
"url": "https://github.com/Dhii/module-interface.git",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dhii/module-interface/zipball/0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"shasum": ""
},
"require": {
"container-interop/service-provider": "^0.4",
"php": "^7.1 | ^8.0",
"psr/container": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 | ^8.0 | ^9.0",
"slevomat/coding-standard": "^6.0",
"vimeo/psalm": "^3.11.7 | ^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-develop": "0.3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dhii\\Modular\\Module\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dhii Team",
"email": "development@dhii.co"
}
],
"description": "Interfaces for modules",
"support": {
"issues": "https://github.com/Dhii/module-interface/issues",
"source": "https://github.com/Dhii/module-interface/tree/v0.3.0-alpha2"
},
"time": "2021-08-23T08:23:01+00:00"
},
{
"name": "psr/container",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.2"
},
"time": "2021-11-05T16:50:12+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^7.2 | ^8.0"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -0,0 +1,12 @@
<?php
/**
* The Pay Later WooCommerce Blocks module extensions.
*
* @package WooCommerce\PayPalCommerce\PayLaterWCBlocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
return array();

View file

@ -0,0 +1,16 @@
<?php
/**
* The Pay Later WooCommerce Blocks module.
*
* @package WooCommerce\PayPalCommerce\PayLaterWCBlocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new PayLaterWCBlocksModule();
};

View file

@ -0,0 +1,36 @@
{
"name": "ppcp-paylater-wc-blocks",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"@paypal/react-paypal-js": "^8.2.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"
}
}

View file

@ -0,0 +1,33 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "woocommerce-paypal-payments/cart-paylater-messages",
"title": "Cart - PayPal Pay Later messaging",
"category": "woocommerce-paypal-payments",
"description": "PayPal Pay Later messaging will be displayed for eligible customers. Customers automatically see the most relevant Pay Later offering.",
"example": {},
"parent": [ "woocommerce/cart-totals-block" ],
"attributes": {
"blockId": {
"type": "string",
"default": "woocommerce-paypal-payments/cart-paylater-messages"
},
"ppcpId": {
"type": "string"
},
"lock": {
"type": "object",
"default": {
"remove": true,
"move": false
}
}
},
"supports": {
"html": false,
"inserter": false,
"multiple": false
},
"textdomain": "woocommerce-paypal-payments",
"editorScript": "ppcp-cart-paylater-block"
}

View file

@ -0,0 +1,141 @@
(function(wp) {
const { createBlock } = wp.blocks;
const { select, dispatch, subscribe } = wp.data;
const getBlocks = () => select('core/block-editor').getBlocks() || [];
const { addFilter } = wp.hooks;
const { assign } = lodash;
// We need to make sure the block is unlocked so that it doesn't get automatically inserted as the last block
addFilter(
'blocks.registerBlockType',
'woocommerce-paypal-payments/modify-cart-paylater-messages',
(settings, name) => {
if (name === 'woocommerce-paypal-payments/cart-paylater-messages') {
const newAttributes = assign({}, settings.attributes, {
lock: assign({}, settings.attributes.lock, {
default: assign({}, settings.attributes.lock.default, {
remove: false
})
})
});
return assign({}, settings, {
attributes: newAttributes
});
}
return settings;
}
);
/**
* Subscribes to changes in the block editor, specifically checking for the presence of 'woocommerce/cart'.
*/
subscribe(() => {
const currentBlocks = getBlocks();
currentBlocks.forEach(block => {
if (block.name === 'woocommerce/cart') {
ensurePayLaterBlockExists(block);
}
});
});
/**
* Ensures the 'woocommerce-paypal-payments/cart-paylater-messages' block exists inside the 'woocommerce/cart' block.
* @param {Object} cartBlock - The cart block instance.
*/
function ensurePayLaterBlockExists(cartBlock) {
const payLaterBlock = findBlockByName(cartBlock.innerBlocks, 'woocommerce-paypal-payments/cart-paylater-messages');
if (!payLaterBlock) {
waitForBlock('woocommerce/cart-totals-block', 'woocommerce-paypal-payments/cart-paylater-messages', 'woocommerce/cart-order-summary-block');
}
}
/**
* Waits for a specific block to appear using async/await pattern before executing the insertBlockAfter function.
* @param {string} targetBlockName - Name of the block to wait for.
* @param {string} newBlockName - Name of the new block to insert after the target.
* @param {string} anchorBlockName - Name of the anchor block to determine position.
* @param {number} attempts - The number of attempts made to find the target block.
*/
async function waitForBlock(targetBlockName, newBlockName, anchorBlockName = '', attempts = 0) {
const targetBlock = findBlockByName(getBlocks(), targetBlockName);
if (targetBlock) {
await delay(1000); // We need this to ensure the block is fully rendered
insertBlockAfter(targetBlockName, newBlockName, anchorBlockName);
} else if (attempts < 10) { // Poll up to 10 times
await delay(1000); // Wait 1 second before retrying
await waitForBlock(targetBlockName, newBlockName, anchorBlockName, attempts + 1);
} else {
console.log('Failed to find target block after several attempts.');
}
}
/**
* Delays execution by a given number of milliseconds.
* @param {number} ms - Milliseconds to delay.
* @return {Promise} A promise that resolves after the delay.
*/
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Inserts a block after a specified block if it doesn't already exist.
* @param {string} targetBlockName - Name of the block to find.
* @param {string} newBlockName - Name of the new block to insert.
* @param {string} anchorBlockName - Name of the anchor block to determine position.
*/
function insertBlockAfter(targetBlockName, newBlockName, anchorBlockName = '') {
const targetBlock = findBlockByName(getBlocks(), targetBlockName);
if (!targetBlock) {
// Target block not found
return;
}
const parentBlock = select('core/block-editor').getBlock(targetBlock.clientId);
if (parentBlock.innerBlocks.some(block => block.name === newBlockName)) {
// The block is already inserted next to the target block
return;
}
let offset = 0;
if (anchorBlockName !== '') {
// Find the anchor block and calculate the offset
const anchorIndex = parentBlock.innerBlocks.findIndex(block => block.name === anchorBlockName);
offset = parentBlock.innerBlocks.length - (anchorIndex + 1);
}
const newBlock = createBlock(newBlockName);
// Insert the block at the correct position
dispatch('core/block-editor').insertBlock(newBlock, parentBlock.innerBlocks.length - offset, parentBlock.clientId);
// Lock the block after it has been inserted
dispatch('core/block-editor').updateBlockAttributes(newBlock.clientId, {
lock: { remove: true }
});
}
/**
* Recursively searches for a block by name among all blocks.
* @param {Array} blocks - The array of blocks to search.
* @param {string} blockName - The name of the block to find.
* @returns {Object|null} The found block, or null if not found.
*/
function findBlockByName(blocks, blockName) {
for (const block of blocks) {
if (block.name === blockName) {
return block;
}
if (block.innerBlocks.length > 0) {
const foundBlock = findBlockByName(block.innerBlocks, blockName);
if (foundBlock) {
return foundBlock;
}
}
}
return null;
}
})(window.wp);

View file

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import Edit from './edit';
import metadata from './block.json';
const paypalIcon = (
<svg width="584.798" height="720" viewBox="0 0 154.728 190.5">
<g transform="translate(898.192 276.071)">
<path
clipPath="none"
d="M-837.663-237.968a5.49 5.49 0 0 0-5.423 4.633l-9.013 57.15-8.281 52.514-.005.044.01-.044 8.281-52.514c.421-2.669 2.719-4.633 5.42-4.633h26.404c26.573 0 49.127-19.387 53.246-45.658.314-1.996.482-3.973.52-5.924v-.003h-.003c-6.753-3.543-14.683-5.565-23.372-5.565z"
fill="#001c64"
/>
<path
clipPath="none"
d="M-766.506-232.402c-.037 1.951-.207 3.93-.52 5.926-4.119 26.271-26.673 45.658-53.246 45.658h-26.404c-2.701 0-4.999 1.964-5.42 4.633l-8.281 52.514-5.197 32.947a4.46 4.46 0 0 0 4.405 5.153h28.66a5.49 5.49 0 0 0 5.423-4.633l7.55-47.881c.423-2.669 2.722-4.636 5.423-4.636h16.876c26.573 0 49.124-19.386 53.243-45.655 2.924-18.649-6.46-35.614-22.511-44.026z"
fill="#0070e0"
/>
<path
clipPath="none"
d="M-870.225-276.071a5.49 5.49 0 0 0-5.423 4.636l-22.489 142.608a4.46 4.46 0 0 0 4.405 5.156h33.351l8.281-52.514 9.013-57.15a5.49 5.49 0 0 1 5.423-4.633h47.782c8.691 0 16.621 2.025 23.375 5.563.46-23.917-19.275-43.666-46.412-43.666z"
fill="#003087"
/>
</g>
</svg>
);
registerBlockType(metadata, {
icon: paypalIcon,
edit: Edit,
save() {
return null;
},
});

View file

@ -0,0 +1,138 @@
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, Spinner } from '@wordpress/components';
import { PayPalScriptProvider, PayPalMessages } from '@paypal/react-paypal-js';
import { useScriptParams } from '../../../../ppcp-paylater-block/resources/js/hooks/script-params';
export default function Edit({ attributes, clientId, setAttributes }) {
const { ppcpId } = attributes;
const [loaded, setLoaded] = useState(false);
let amount;
const postContent = String(wp.data.select('core/editor')?.getEditedPostContent());
if (postContent.includes('woocommerce/checkout') || postContent.includes('woocommerce/cart')) {
amount = 50.0;
}
const cartConfig = PcpCartPayLaterBlock.config.cart;
// Dynamically setting previewStyle based on the layout attribute
let previewStyle = {};
if (cartConfig.layout === 'flex') {
previewStyle = {
layout: cartConfig.layout,
color: cartConfig.color,
ratio: cartConfig.ratio,
};
} else {
previewStyle = {
layout: cartConfig.layout,
logo: {
position: cartConfig['logo-position'],
type: cartConfig['logo-type'],
},
text: {
color: cartConfig['text-color'],
size: cartConfig['text-size'],
},
};
}
let classes = ['ppcp-paylater-block-preview', 'ppcp-overlay-parent'];
if (PcpCartPayLaterBlock.vaultingEnabled || !PcpCartPayLaterBlock.placementEnabled) {
classes = [...classes, 'ppcp-paylater-unavailable', 'block-editor-warning'];
}
const props = useBlockProps({ className: classes.join(' ') });
useEffect(() => {
if (!ppcpId) {
setAttributes({ ppcpId: `ppcp-${clientId}` });
}
}, [ppcpId, clientId]);
if (PcpCartPayLaterBlock.vaultingEnabled) {
return (
<div {...props}>
<div className='block-editor-warning__contents'>
<p className='block-editor-warning__message'>
{__('Cart - Pay Later Messaging cannot be used while PayPal Vaulting is active. Disable PayPal Vaulting in the PayPal Payment settings to reactivate this block', 'woocommerce-paypal-payments')}
</p>
<div className='block-editor-warning__actions'>
<span className='block-editor-warning__action'>
<a href={PcpCartPayLaterBlock.settingsUrl}>
<button type='button' className='components-button is-primary'>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</span>
</div>
</div>
</div>
);
}
if (!PcpCartPayLaterBlock.placementEnabled) {
return (
<div {...props}>
<div className='block-editor-warning__contents'>
<p className='block-editor-warning__message'>
{__('Cart - Pay Later Messaging cannot be used while the “Cart” messaging placement is disabled. Enable the placement in the PayPal Payments Pay Later settings to reactivate this block.', 'woocommerce-paypal-payments')}
</p>
<div className='block-editor-warning__actions'>
<span className='block-editor-warning__action'>
<a href={PcpCartPayLaterBlock.payLaterSettingsUrl}>
<button type='button' className='components-button is-primary'>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</span>
</div>
</div>
</div>
);
}
const scriptParams = useScriptParams(PcpCartPayLaterBlock.ajax.cart_script_params);
if (scriptParams === null) {
return <div {...props}><Spinner /></div>;
}
const urlParams = {
...scriptParams.url_params,
components: 'messages',
dataNamespace: 'ppcp-block-editor-cart-paylater-message',
};
return (
<>
<InspectorControls>
<PanelBody title={__('Customize your messaging', 'woocommerce-paypal-payments')}>
<p>
{__('Choose the layout and color of your messaging in the PayPal Payments Pay Later settings for the “Cart” messaging placement.', 'woocommerce-paypal-payments')}
</p>
<a href={PcpCartPayLaterBlock.payLaterSettingsUrl}>
<button type='button' className='components-button is-primary'>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</PanelBody>
</InspectorControls>
<div {...props}>
<div className='ppcp-overlay-child'>
<PayPalScriptProvider options={urlParams}>
<PayPalMessages
style={previewStyle}
onRender={() => setLoaded(true)}
amount={amount}
/>
</PayPalScriptProvider>
</div>
<div className='ppcp-overlay-child ppcp-unclicable-overlay'> {/* make the message not clickable */}
{!loaded && <Spinner />}
</div>
</div>
</>
);
}

View file

@ -0,0 +1,33 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "woocommerce-paypal-payments/checkout-paylater-messages",
"title": "Checkout - PayPal Pay Later messaging",
"category": "woocommerce-paypal-payments",
"description": "PayPal Pay Later messaging will be displayed for eligible customers. Customers automatically see the most relevant Pay Later offering.",
"example": {},
"parent": [ "woocommerce/checkout-totals-block" ],
"attributes": {
"blockId": {
"type": "string",
"default": "woocommerce-paypal-payments/checkout-paylater-messages"
},
"ppcpId": {
"type": "string"
},
"lock": {
"type": "object",
"default": {
"remove": true,
"move": false
}
}
},
"supports": {
"html": false,
"inserter": false,
"multiple": false
},
"textdomain": "woocommerce-paypal-payments",
"editorScript": "ppcp-checkout-paylater-block"
}

View file

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import Edit from './edit';
import metadata from './block.json';
const paypalIcon = (
<svg width="584.798" height="720" viewBox="0 0 154.728 190.5">
<g transform="translate(898.192 276.071)">
<path
clipPath="none"
d="M-837.663-237.968a5.49 5.49 0 0 0-5.423 4.633l-9.013 57.15-8.281 52.514-.005.044.01-.044 8.281-52.514c.421-2.669 2.719-4.633 5.42-4.633h26.404c26.573 0 49.127-19.387 53.246-45.658.314-1.996.482-3.973.52-5.924v-.003h-.003c-6.753-3.543-14.683-5.565-23.372-5.565z"
fill="#001c64"
/>
<path
clipPath="none"
d="M-766.506-232.402c-.037 1.951-.207 3.93-.52 5.926-4.119 26.271-26.673 45.658-53.246 45.658h-26.404c-2.701 0-4.999 1.964-5.42 4.633l-8.281 52.514-5.197 32.947a4.46 4.46 0 0 0 4.405 5.153h28.66a5.49 5.49 0 0 0 5.423-4.633l7.55-47.881c.423-2.669 2.722-4.636 5.423-4.636h16.876c26.573 0 49.124-19.386 53.243-45.655 2.924-18.649-6.46-35.614-22.511-44.026z"
fill="#0070e0"
/>
<path
clipPath="none"
d="M-870.225-276.071a5.49 5.49 0 0 0-5.423 4.636l-22.489 142.608a4.46 4.46 0 0 0 4.405 5.156h33.351l8.281-52.514 9.013-57.15a5.49 5.49 0 0 1 5.423-4.633h47.782c8.691 0 16.621 2.025 23.375 5.563.46-23.917-19.275-43.666-46.412-43.666z"
fill="#003087"
/>
</g>
</svg>
);
registerBlockType(metadata, {
icon: paypalIcon,
edit: Edit,
save() {
return null;
},
});

View file

@ -0,0 +1,132 @@
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, Spinner } from '@wordpress/components';
import { PayPalScriptProvider, PayPalMessages } from '@paypal/react-paypal-js';
import { useScriptParams } from '../../../../ppcp-paylater-block/resources/js/hooks/script-params';
export default function Edit({ attributes, clientId, setAttributes }) {
const { ppcpId } = attributes;
const [loaded, setLoaded] = useState(false);
let amount = undefined;
const postContent = String(wp.data.select('core/editor')?.getEditedPostContent());
if (postContent.includes('woocommerce/checkout') || postContent.includes('woocommerce/cart')) {
amount = 50.0;
}
const checkoutConfig = PcpCheckoutPayLaterBlock.config.checkout;
// Dynamically setting previewStyle based on the layout attribute
let previewStyle = {};
if (checkoutConfig.layout === 'flex') {
previewStyle = {
layout: checkoutConfig.layout,
color: checkoutConfig.color,
ratio: checkoutConfig.ratio,
};
} else {
previewStyle = {
layout: checkoutConfig.layout,
logo: {
position: checkoutConfig['logo-position'],
type: checkoutConfig['logo-type'],
},
text: {
color: checkoutConfig['text-color'],
size: checkoutConfig['text-size'],
},
};
}
let classes = ['ppcp-paylater-block-preview', 'ppcp-overlay-parent'];
if (PcpCheckoutPayLaterBlock.vaultingEnabled || !PcpCheckoutPayLaterBlock.placementEnabled) {
classes = ['ppcp-paylater-block-preview', 'ppcp-paylater-unavailable', 'block-editor-warning'];
}
const props = useBlockProps({ className: classes });
useEffect(() => {
if (!ppcpId) {
setAttributes({ ppcpId: 'ppcp-' + clientId });
}
}, [ppcpId, clientId]);
if (PcpCheckoutPayLaterBlock.vaultingEnabled) {
return (
<div {...props}>
<div className={'block-editor-warning__contents'}>
<p className={'block-editor-warning__message'}>{__('Checkout - Pay Later Messaging cannot be used while PayPal Vaulting is active. Disable PayPal Vaulting in the PayPal Payment settings to reactivate this block', 'woocommerce-paypal-payments')}</p>
<div className={'block-editor-warning__actions'}>
<span className={'block-editor-warning__action'}>
<a href={PcpCheckoutPayLaterBlock.settingsUrl}>
<button type={'button'} className={'components-button is-primary'}>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</span>
</div>
</div>
</div>
);
}
if (!PcpCheckoutPayLaterBlock.placementEnabled) {
return (
<div {...props}>
<div className={'block-editor-warning__contents'}>
<p className={'block-editor-warning__message'}>{__('Checkout - Pay Later Messaging cannot be used while the “Checkout” messaging placement is disabled. Enable the placement in the PayPal Payments Pay Later settings to reactivate this block.', 'woocommerce-paypal-payments')}</p>
<div className={'block-editor-warning__actions'}>
<span className={'block-editor-warning__action'}>
<a href={PcpCheckoutPayLaterBlock.payLaterSettingsUrl}>
<button type={'button'} className={'components-button is-primary'}>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</span>
</div>
</div>
</div>
);
}
const scriptParams = useScriptParams(PcpCheckoutPayLaterBlock.ajax.cart_script_params);
if (scriptParams === null) {
return <div {...props}><Spinner/></div>;
}
const urlParams = {
...scriptParams.url_params,
components: 'messages',
dataNamespace: 'ppcp-block-editor-checkout-paylater-message',
};
return (
<>
<InspectorControls>
<PanelBody title={__('Customize your messaging', 'woocommerce-paypal-payments')}>
<p>{__('Choose the layout and color of your messaging in the PayPal Payments Pay Later settings for the “Checkout” messaging placement.', 'woocommerce-paypal-payments')}</p>
<a href={PcpCheckoutPayLaterBlock.payLaterSettingsUrl}>
<button type={'button'} className={'components-button is-primary'}>
{__('PayPal Payments Settings', 'woocommerce-paypal-payments')}
</button>
</a>
</PanelBody>
</InspectorControls>
<div {...props}>
<div className={'ppcp-overlay-child'}>
<PayPalScriptProvider options={urlParams}>
<PayPalMessages
style={previewStyle}
onRender={() => setLoaded(true)}
amount={amount}
/>
</PayPalScriptProvider>
</div>
<div className={'ppcp-overlay-child ppcp-unclicable-overlay'}> {/* make the message not clickable */}
{!loaded && <Spinner/>}
</div>
</div>
</>
);
}

View file

@ -0,0 +1,58 @@
<?php
/**
* The Pay Later WooCommerce Blocks module services.
*
* @package WooCommerce\PayPalCommerce\PayLaterWCBlocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'paylater-wc-blocks.url' => static function ( ContainerInterface $container ): string {
/**
* Cannot return false for this path.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-paylater-wc-blocks/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'paylater-wc-blocks.cart-renderer' => static function ( ContainerInterface $container ): PayLaterWCBlocksRenderer {
$settings = $container->get( 'wcgateway.settings' );
return new PayLaterWCBlocksRenderer(
array(
'placement' => 'cart',
'layout' => $settings->has( 'pay_later_cart_message_layout' ) ? $settings->get( 'pay_later_cart_message_layout' ) : '',
'position' => $settings->has( 'pay_later_cart_message_position' ) ? $settings->get( 'pay_later_cart_message_position' ) : '',
'logo' => $settings->has( 'pay_later_cart_message_logo' ) ? $settings->get( 'pay_later_cart_message_logo' ) : '',
'text_size' => $settings->has( 'pay_later_cart_message_text_size' ) ? $settings->get( 'pay_later_cart_message_text_size' ) : '',
'color' => $settings->has( 'pay_later_cart_message_color' ) ? $settings->get( 'pay_later_cart_message_color' ) : '',
'flex_color' => $settings->has( 'pay_later_cart_message_flex_color' ) ? $settings->get( 'pay_later_cart_message_flex_color' ) : '',
'flex_ratio' => $settings->has( 'pay_later_cart_message_flex_ratio' ) ? $settings->get( 'pay_later_cart_message_flex_ratio' ) : '',
)
);
},
'paylater-wc-blocks.checkout-renderer' => static function ( ContainerInterface $container ): PayLaterWCBlocksRenderer {
$settings = $container->get( 'wcgateway.settings' );
return new PayLaterWCBlocksRenderer(
array(
'payment',
'layout' => $settings->has( 'pay_later_checkout_message_layout' ) ? $settings->get( 'pay_later_checkout_message_layout' ) : '',
'position' => $settings->has( 'pay_later_checkout_message_position' ) ? $settings->get( 'pay_later_checkout_message_position' ) : '',
'logo' => $settings->has( 'pay_later_checkout_message_logo' ) ? $settings->get( 'pay_later_checkout_message_logo' ) : '',
'text_size' => $settings->has( 'pay_later_checkout_message_text_size' ) ? $settings->get( 'pay_later_checkout_message_text_size' ) : '',
'color' => $settings->has( 'pay_later_checkout_message_color' ) ? $settings->get( 'pay_later_checkout_message_color' ) : '',
'flex_color' => $settings->has( 'pay_later_checkout_message_flex_color' ) ? $settings->get( 'pay_later_checkout_message_flex_color' ) : '',
'flex_ratio' => $settings->has( 'pay_later_checkout_message_flex_ratio' ) ? $settings->get( 'pay_later_checkout_message_flex_ratio' ) : '',
)
);
},
);

View file

@ -0,0 +1,293 @@
<?php
/**
* The Pay Later WooCommerce Blocks module.
*
* @package WooCommerce\PayPalCommerce\PayLaterWCBlocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksUtils;
/**
* Class PayLaterWCBlocksModule
*/
class PayLaterWCBlocksModule implements ModuleInterface {
/**
* Returns whether the block module should be loaded.
*
* @return bool true if the module should be loaded, otherwise false.
*/
public static function is_module_loading_required(): bool {
return apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_wc_blocks_enabled',
getenv( 'PCP_PAYLATER_WC_BLOCKS' ) !== '0'
);
}
/**
* Returns whether the block is enabled.
*
* @param SettingsStatus $settings_status The Settings status helper.
* @param string $location The location to check.
* @return bool true if the block is enabled, otherwise false.
*/
public static function is_block_enabled( SettingsStatus $settings_status, string $location ): bool {
return self::is_module_loading_required() && $settings_status->is_pay_later_messaging_enabled_for_location( $location );
}
/**
* Returns whether the placement is enabled.
*
* @param SettingsStatus $settings_status The Settings status helper.
* @param string $location The location to check.
* @return bool true if the placement is enabled, otherwise false.
*/
public static function is_placement_enabled( SettingsStatus $settings_status, string $location ) : bool {
return self::is_block_enabled( $settings_status, $location );
}
/**
* Returns whether the under cart totals placement is enabled.
*
* @return bool true if the under cart totals placement is enabled, otherwise false.
*/
public function is_under_cart_totals_placement_enabled() : bool {
return apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_wc_blocks_cart_under_totals_enabled',
true
);
}
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
$messages_apply = $c->get( 'button.helper.messages-apply' );
assert( $messages_apply instanceof MessagesApply );
if ( ! $messages_apply->for_country() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
add_action(
'init',
function () use ( $c, $settings ): void {
$config_factory = $c->get( 'paylater-configurator.factory.config' );
assert( $config_factory instanceof ConfigFactory );
$script_handle = 'ppcp-cart-paylater-block';
wp_register_script(
$script_handle,
$c->get( 'paylater-wc-blocks.url' ) . 'assets/js/cart-paylater-block.js',
array(),
$c->get( 'ppcp.asset-version' ),
true
);
wp_localize_script(
$script_handle,
'PcpCartPayLaterBlock',
array(
'ajax' => array(
'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
),
),
'config' => $config_factory->from_settings( $settings ),
'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' ),
'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ),
'placementEnabled' => self::is_placement_enabled( $c->get( 'wcgateway.settings.status' ), 'cart' ),
'payLaterSettingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-pay-later' ),
'underTotalsPlacementEnabled' => self::is_under_cart_totals_placement_enabled(),
)
);
$script_handle = 'ppcp-checkout-paylater-block';
wp_register_script(
$script_handle,
$c->get( 'paylater-wc-blocks.url' ) . 'assets/js/checkout-paylater-block.js',
array(),
$c->get( 'ppcp.asset-version' ),
true
);
wp_localize_script(
$script_handle,
'PcpCheckoutPayLaterBlock',
array(
'ajax' => array(
'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
),
),
'config' => $config_factory->from_settings( $settings ),
'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' ),
'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ),
'placementEnabled' => self::is_placement_enabled( $c->get( 'wcgateway.settings.status' ), 'checkout' ),
'payLaterSettingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-pay-later' ),
)
);
},
20
);
/**
* Registers slugs as block categories with WordPress.
*/
add_action(
'block_categories_all',
function ( array $categories ): array {
return array_merge(
$categories,
array(
array(
'slug' => 'woocommerce-paypal-payments',
'title' => __( 'PayPal Blocks', 'woocommerce-paypal-payments' ),
),
)
);
},
10,
2
);
/**
* Cannot return false for this path.
*
* @psalm-suppress PossiblyFalseArgument
*/
if ( function_exists( 'register_block_type' ) ) {
register_block_type(
dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CartPayLaterMessagesBlock',
array(
'render_callback' => function ( array $attributes ) use ( $c ) {
return PayLaterWCBlocksUtils::render_paylater_block(
$attributes['blockId'] ?? 'woocommerce-paypal-payments/cart-paylater-messages',
$attributes['ppcpId'] ?? 'ppcp-cart-paylater-messages',
'cart',
$c
);
},
)
);
}
/**
* Cannot return false for this path.
*
* @psalm-suppress PossiblyFalseArgument
*/
if ( function_exists( 'register_block_type' ) ) {
register_block_type(
dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CheckoutPayLaterMessagesBlock',
array(
'render_callback' => function ( array $attributes ) use ( $c ) {
return PayLaterWCBlocksUtils::render_paylater_block(
$attributes['blockId'] ?? 'woocommerce-paypal-payments/checkout-paylater-messages',
$attributes['ppcpId'] ?? 'ppcp-checkout-paylater-messages',
'checkout',
$c
);
},
)
);
}
// This is a fallback for the default Cart block that haven't been saved with the inserted Pay Later messaging block.
add_filter(
'render_block_woocommerce/cart-totals-block',
function ( string $block_content ) use ( $c ) {
if ( false === strpos( $block_content, 'woocommerce-paypal-payments/cart-paylater-messages' ) ) {
return PayLaterWCBlocksUtils::render_and_insert_paylater_block(
$block_content,
'woocommerce-paypal-payments/cart-paylater-messages',
'ppcp-cart-paylater-messages',
'cart',
$c,
self::is_under_cart_totals_placement_enabled()
);
}
return $block_content;
},
10,
1
);
// This is a fallback for the default Checkout block that haven't been saved with the inserted Checkout - Pay Later messaging block.
add_filter(
'render_block_woocommerce/checkout-totals-block',
function ( string $block_content ) use ( $c ) {
if ( false === strpos( $block_content, 'woocommerce-paypal-payments/checkout-paylater-messages' ) ) {
return PayLaterWCBlocksUtils::render_and_insert_paylater_block(
$block_content,
'woocommerce-paypal-payments/checkout-paylater-messages',
'ppcp-checkout-paylater-messages',
'checkout',
$c
);
}
return $block_content;
},
10,
1
);
// Since there's no regular way we can place the Pay Later messaging block under the cart totals block, we need a custom script.
if ( self::is_under_cart_totals_placement_enabled() ) {
add_action(
'enqueue_block_editor_assets',
function () use ( $c, $settings ): void {
$handle = 'ppcp-checkout-paylater-block-editor-inserter';
$path = $c->get( 'paylater-wc-blocks.url' ) . 'assets/js/cart-paylater-block-inserter.js';
wp_register_script(
$handle,
$path,
array( 'wp-blocks', 'wp-data', 'wp-element' ),
$c->get( 'ppcp.asset-version' ),
true
);
wp_enqueue_script( $handle );
}
);
}
}
/**
* Returns the key for the module.
*
* @return void
*/
public function getKey() {
}
}

View file

@ -0,0 +1,131 @@
<?php
/**
* The Pay Later WooCommerce Blocks Renderer.
*
* @package WooCommerce\PayPalCommerce\PayLaterWCBlocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class PayLaterWCBlocksRenderer
*/
class PayLaterWCBlocksRenderer {
/**
* The placement.
*
* @var string
*/
private $placement;
/**
* The layout.
*
* @var string
*/
private $layout;
/**
* The position.
*
* @var string
*/
private $position;
/**
* The logo.
*
* @var string
*/
private $logo;
/**
* The text size.
*
* @var string
*/
private $text_size;
/**
* The color.
*
* @var string
*/
private $color;
/**
* The flex color.
*
* @var string
*/
private $flex_color;
/**
* The flex ratio.
*
* @var string
*/
private $flex_ratio;
/**
* PayLaterWCBlocksRenderer constructor.
*
* @param array $config The configuration.
*/
public function __construct( array $config ) {
$this->placement = $config['placement'] ?? '';
$this->layout = $config['layout'] ?? 'text';
$this->position = $config['position'] ?? '';
$this->logo = $config['logo'] ?? '';
$this->text_size = $config['text_size'] ?? '';
$this->color = $config['color'] ?? '';
$this->flex_color = $config['flex_color'] ?? '';
$this->flex_ratio = $config['flex_ratio'] ?? '';
}
/**
* Renders the WC Pay Later Messaging blocks.
*
* @param array $attributes The block attributes.
* @param string $location The location of the block.
* @param ContainerInterface $c The container.
* @return string|void
*/
public function render(
array $attributes,
string $location,
ContainerInterface $c
) {
if ( PayLaterWCBlocksModule::is_placement_enabled( $c->get( 'wcgateway.settings.status' ), $location ) ) {
$html = '<div id="' . esc_attr( $attributes['ppcpId'] ?? '' ) . '" class="ppcp-messages" data-partner-attribution-id="Woo_PPCP"></div>';
$processor = new \WP_HTML_Tag_Processor( $html );
if ( $processor->next_tag( 'div' ) ) {
$processor->set_attribute( 'data-block-name', esc_attr( $attributes['blockId'] ?? '' ) );
$processor->set_attribute( 'class', 'ppcp-messages' );
$processor->set_attribute( 'data-partner-attribution-id', 'Woo_PPCP' );
if ( $this->layout === 'flex' ) {
$processor->set_attribute( 'data-pp-style-layout', 'flex' );
$processor->set_attribute( 'data-pp-style-color', esc_attr( $this->flex_color ) );
$processor->set_attribute( 'data-pp-style-ratio', esc_attr( $this->flex_ratio ) );
} else {
$processor->set_attribute( 'data-pp-style-layout', 'text' );
$processor->set_attribute( 'data-pp-style-logo-type', esc_attr( $this->logo ) );
$processor->set_attribute( 'data-pp-style-logo-position', esc_attr( $this->position ) );
$processor->set_attribute( 'data-pp-style-text-color', esc_attr( $this->color ) );
$processor->set_attribute( 'data-pp-style-text-size', esc_attr( $this->text_size ) );
}
$processor->set_attribute( 'data-pp-placement', esc_attr( $this->placement ) );
}
$updated_html = $processor->get_updated_html();
return sprintf(
'<div %1$s>%2$s</div>',
wp_kses_data( get_block_wrapper_attributes() ),
$updated_html
);
}
}
}

View file

@ -0,0 +1,104 @@
<?php
/**
* The Pay Later WooCommerce Blocks Utils.
*
* @package WooCommerce\PayPalCommerce\PayLaterWCBlocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayLaterWCBlocks;
/**
* Class PayLaterWCBlocksUtils
*/
class PayLaterWCBlocksUtils {
/**
* Inserts content before the last div in a block.
*
* @param string $block_content The block content.
* @param string $content_to_insert The content to insert.
* @return string The block content with the content inserted.
*/
public static function insert_before_last_div( string $block_content, string $content_to_insert ): string {
$last_index = strrpos( $block_content, '</div>' );
if ( false !== $last_index ) {
$block_content = substr_replace( $block_content, $content_to_insert, $last_index, 0 );
}
return $block_content;
}
/**
* Inserts content after the closing div tag of a specific block.
*
* @param string $block_content The block content.
* @param string $content_to_insert The content to insert.
* @param string $reference_block The block markup to insert the content after.
* @return string The block content with the content inserted.
*/
public static function insert_before_opening_div( string $block_content, string $content_to_insert, string $reference_block ): string {
$reference_block_index = strpos( $block_content, $reference_block );
if ( false !== $reference_block_index ) {
return substr_replace( $block_content, $content_to_insert, $reference_block_index, 0 );
} else {
return self::insert_before_last_div( $block_content, $content_to_insert );
}
}
/**
* Renders a PayLater message block and inserts it before the last closing div tag if the block id is not already present.
*
* @param string $block_content Current content of the block.
* @param string $block_id ID of the block to render.
* @param string $ppcp_id ID for the PPCP component.
* @param string $context Rendering context (cart or checkout).
* @param mixed $container Dependency injection container.
* @param bool $is_under_cart_totals_placement_enabled Whether the block should be placed under the cart totals.
* @return string Updated block content.
*/
public static function render_and_insert_paylater_block( string $block_content, string $block_id, string $ppcp_id, string $context, $container, bool $is_under_cart_totals_placement_enabled = false ): string {
$paylater_message_block = self::render_paylater_block( $block_id, $ppcp_id, $context, $container );
$cart_express_payment_block = '<div data-block-name="woocommerce/cart-express-payment-block" class="wp-block-woocommerce-cart-express-payment-block"></div>';
if ( false !== $paylater_message_block ) {
if ( $is_under_cart_totals_placement_enabled && $context === 'cart' ) {
return self::insert_before_opening_div( $block_content, $paylater_message_block, $cart_express_payment_block );
} else {
return self::insert_before_last_div( $block_content, $paylater_message_block );
}
}
return $block_content;
}
/**
* Renders the PayLater block based on the provided parameters.
*
* @param string $block_id ID of the block to render.
* @param string $ppcp_id ID for the PPCP component.
* @param string $context Rendering context (cart or checkout).
* @param mixed $container Dependency injection container.
* @return false|string Rendered content.
*/
public static function render_paylater_block( string $block_id, string $ppcp_id, string $context, $container ) {
$renderer = $container->get( 'paylater-wc-blocks.' . $context . '-renderer' );
ob_start();
// phpcs:ignore -- No need to escape it, the PayLaterWCBlocksRenderer class is responsible for escaping.
echo $renderer->render(
array(
// phpcs:ignore
'blockId' => $block_id,
// phpcs:ignore
'ppcpId' => $ppcp_id,
),
// phpcs:ignore
$context,
// phpcs:ignore
$container
);
return ob_get_clean();
}
}

View file

@ -0,0 +1,63 @@
const path = require("path");
const isProduction = process.env.NODE_ENV === "production";
const DependencyExtractionWebpackPlugin = require("@woocommerce/dependency-extraction-webpack-plugin");
module.exports = {
devtool: isProduction ? "source-map" : "eval-source-map",
mode: isProduction ? "production" : "development",
target: "web",
plugins: [new DependencyExtractionWebpackPlugin()],
entry: {
"cart-paylater-block": path.resolve(
process.cwd(),
"resources",
"js",
"CartPayLaterMessagesBlock",
"cart-paylater-block.js"
),
"cart-paylater-block-inserter": path.resolve(
process.cwd(),
"resources",
"js",
"CartPayLaterMessagesBlock",
"cart-paylater-block-inserter.js"
),
"checkout-paylater-block": path.resolve(
process.cwd(),
"resources",
"js",
"CheckoutPayLaterMessagesBlock",
"checkout-paylater-block.js"
),
},
output: {
path: path.resolve(__dirname, "assets/"),
filename: "js/[name].js",
},
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: "file-loader",
options: {
name: "css/[name].css",
},
},
{ loader: "sass-loader" },
],
},
],
},
};

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,8 @@
@use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button;
$border-color: #c3c3c3;
$background-ident-color: #fbfbfb;
.ppcp-field-hidden {
display: none !important;
}
@ -11,9 +14,10 @@
opacity: 0.5;
}
.ppcp-field-indent {
th {
padding-left: 20px;
.ppcp-active-spacer {
th, td {
padding: 0;
height: 1rem;
}
}
@ -39,3 +43,57 @@
font-weight: bold;
}
}
.ppcp-align-label-center {
th {
text-align: center;
}
}
.ppcp-valign-label-middle {
th {
vertical-align: middle;
}
}
// Box indented fields.
@media screen and (min-width: 800px) {
.ppcp-settings-field {
border-left: 1px solid transparent;
border-right: 1px solid transparent;
&.active {
background-color: $background-ident-color;
border: 1px solid $border-color;
th {
padding-left: 20px;
}
}
&.ppcp-field-indent {
background-color: $background-ident-color;
border: 1px solid $border-color;
th, &.ppcp-settings-field-heading td {
padding-left: 40px;
}
th, td {
border-top: 1px solid $border-color;
}
& + .ppcp-field-indent {
th, td {
border-top: 1px solid $background-ident-color;
}
}
}
}
.ppcp-settings-field-select {
p.description {
margin-bottom: 1em;
}
}
}

View file

@ -1,10 +1,13 @@
import ElementAction from "./action/ElementAction";
import VisibilityAction from "./action/VisibilityAction";
import AttributeAction from "./action/AttributeAction";
class ActionFactory {
static make(actionConfig) {
switch (actionConfig.type) {
case 'element':
return new ElementAction(actionConfig);
case 'visibility':
return new VisibilityAction(actionConfig);
case 'attribute':
return new AttributeAction(actionConfig);
}
throw new Error('[ActionFactory] Unknown action: ' + actionConfig.type);

View file

@ -1,5 +1,6 @@
import ElementCondition from "./condition/ElementCondition";
import BoolCondition from "./condition/BoolCondition";
import JsVariableCondition from "./condition/JsVariableCondition";
class ConditionFactory {
static make(conditionConfig, triggerUpdate) {
@ -8,6 +9,8 @@ class ConditionFactory {
return new ElementCondition(conditionConfig, triggerUpdate);
case 'bool':
return new BoolCondition(conditionConfig, triggerUpdate);
case 'js_variable':
return new JsVariableCondition(conditionConfig, triggerUpdate);
}
throw new Error('[ConditionFactory] Unknown condition: ' + conditionConfig.type);

View file

@ -0,0 +1,17 @@
import BaseAction from "./BaseAction";
class AttributeAction extends BaseAction {
run(status) {
if (status) {
jQuery(this.config.selector).addClass(this.config.html_class);
} else {
jQuery(this.config.selector).removeClass(this.config.html_class);
}
}
}
export default AttributeAction;

View file

@ -1,6 +1,6 @@
import BaseAction from "./BaseAction";
class ElementAction extends BaseAction {
class VisibilityAction extends BaseAction {
run(status) {
@ -32,4 +32,4 @@ class ElementAction extends BaseAction {
}
export default ElementAction;
export default VisibilityAction;

View file

@ -0,0 +1,24 @@
import BaseCondition from "./BaseCondition";
class JsVariableCondition extends BaseCondition {
register() {
jQuery(document).on('ppcp-display-change', () => {
const status = this.check();
if (status !== this.status) {
this.status = status;
this.triggerUpdate();
}
});
this.status = this.check();
}
check() {
let value = document[this.config.variable];
return this.config.value === value;
}
}
export default JsVariableCondition;

View file

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
@ -194,6 +195,7 @@ return array(
CardButtonGateway::ID,
OXXOGateway::ID,
Settings::PAY_LATER_TAB_ID,
AxoGateway::ID,
),
true
);
@ -801,6 +803,14 @@ return array(
'elo' => _x( 'Elo', 'Name of credit card', 'woocommerce-paypal-payments' ),
'hiper' => _x( 'Hiper', 'Name of credit card', 'woocommerce-paypal-payments' ),
),
'options_axo' => array(
'dinersclub-light' => _x( 'Diners Club (light)', 'Name of credit card', 'woocommerce-paypal-payments' ),
'discover-light' => _x( 'Discover (light)', 'Name of credit card', 'woocommerce-paypal-payments' ),
'jcb-light' => _x( 'JCB (light)', 'Name of credit card', 'woocommerce-paypal-payments' ),
'mastercard-light' => _x( 'Mastercard (light)', 'Name of credit card', 'woocommerce-paypal-payments' ),
'unionpay-light' => _x( 'UnionPay (light)', 'Name of credit card', 'woocommerce-paypal-payments' ),
'visa-light' => _x( 'Visa (light)', 'Name of credit card', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_ONBOARDED,
),
@ -809,25 +819,6 @@ return array(
),
'gateway' => 'dcc',
),
'vault_enabled_dcc' => array(
'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Securely store your customers credit cards for a seamless checkout experience and subscription features. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#vaulting-saving-a-payment-method" target="_blank">',
'</a>'
),
'description' => __( 'Allow registered buyers to save Credit Card payments.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ),
),
'3d_secure_heading' => array(
'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
@ -884,6 +875,52 @@ return array(
),
'gateway' => 'dcc',
),
'saved_payments_heading' => array(
'heading' => __( 'Saved Payments', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
'description' => wp_kses_post(
sprintf(
// translators: %1$s and %2$s is a link tag.
__(
'PayPal can securely store your customers payment methods for
%1$sfuture payments and subscriptions%2$s, simplifying the checkout
process and enabling recurring transactions on your website.',
'woocommerce-paypal-payments'
),
'<a
rel="noreferrer noopener"
href="https://woo.com/document/woocommerce-paypal-payments/#vaulting-a-card"
>',
'</a>'
)
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(
'dcc',
),
'gateway' => 'dcc',
),
'vault_enabled_dcc' => array(
'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Securely store your customers credit cards for a seamless checkout experience and subscription features. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#vaulting-saving-a-payment-method" target="_blank">',
'</a>'
),
'description' => __( 'Allow registered buyers to save Credit Card payments.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ),
),
'paypal_saved_payments' => array(
'heading' => __( 'Saved payments', 'woocommerce-paypal-payments' ),
'description' => sprintf(
@ -922,6 +959,32 @@ return array(
'gateway' => 'paypal',
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ),
),
'digital_wallet_heading' => array(
'heading' => __( 'Digital Wallet Services', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
'description' => wp_kses_post(
sprintf(
// translators: %1$s and %2$s is a link tag.
__(
'PayPal supports digital wallet services like Apple Pay or Google Pay
to give your buyers more options to pay without a PayPal account.',
'woocommerce-paypal-payments'
),
'<a
rel="noreferrer noopener"
href="https://woo.com/document/woocommerce-paypal-payments/#vaulting-a-card"
>',
'</a>'
)
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(
'dcc',
),
'gateway' => 'dcc',
),
);
if ( ! $subscription_helper->plugin_is_active() ) {
@ -1509,6 +1572,7 @@ return array(
PayUponInvoiceGateway::ID,
CardButtonGateway::ID,
OXXOGateway::ID,
AxoGateway::ID,
);
},
'wcgateway.gateway-repository' => static function ( ContainerInterface $container ): GatewayRepository {

View file

@ -298,6 +298,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
'subscription_payment_method_change_admin',
'multiple_subscriptions'
);
} elseif ( $this->config->has( 'subscriptions_mode' ) && $this->config->get( 'subscriptions_mode' ) === 'subscriptions_api' ) {
$this->supports[] = 'gateway_scheduled_payments';
} elseif ( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) ) {
$this->supports[] = 'tokenization';
}

View file

@ -16,8 +16,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class DisplayRule {
const CONDITION_TYPE_ELEMENT = 'element';
const CONDITION_TYPE_BOOL = 'bool';
const CONDITION_TYPE_ELEMENT = 'element';
const CONDITION_TYPE_BOOL = 'bool';
const CONDITION_TYPE_JS_VARIABLE = 'js_variable';
const CONDITION_OPERATION_EQUALS = 'equals';
const CONDITION_OPERATION_NOT_EQUALS = 'not_equals';
@ -26,10 +27,12 @@ class DisplayRule {
const CONDITION_OPERATION_EMPTY = 'empty';
const CONDITION_OPERATION_NOT_EMPTY = 'not_empty';
const ACTION_TYPE_ELEMENT = 'element';
const ACTION_TYPE_VISIBILITY = 'visibility';
const ACTION_TYPE_ATTRIBUTE = 'attribute';
const ACTION_VISIBLE = 'visible';
const ACTION_ENABLE = 'enable';
const ACTION_CLASS = 'class';
/**
* The element selector.
@ -132,6 +135,24 @@ class DisplayRule {
return $this;
}
/**
* Adds a condition related to js variable check.
*
* @param string $variable_name The javascript variable name.
* @param mixed $value The value to enable / disable the condition.
* @return self
*/
public function condition_js_variable( string $variable_name, $value ): self {
$this->add_condition(
array(
'type' => self::CONDITION_TYPE_JS_VARIABLE,
'variable' => $variable_name,
'value' => $value,
)
);
return $this;
}
/**
* Adds a condition to show/hide the element.
*
@ -140,7 +161,7 @@ class DisplayRule {
public function action_visible( string $selector ): self {
$this->add_action(
array(
'type' => self::ACTION_TYPE_ELEMENT,
'type' => self::ACTION_TYPE_VISIBILITY,
'selector' => $selector,
'action' => self::ACTION_VISIBLE,
)
@ -148,6 +169,24 @@ class DisplayRule {
return $this;
}
/**
* Adds a condition to add/remove a html class.
*
* @param string $selector The condition selector.
* @param string $class The class.
*/
public function action_class( string $selector, string $class ): self {
$this->add_action(
array(
'type' => self::ACTION_TYPE_ATTRIBUTE,
'selector' => $selector,
'html_class' => $class,
'action' => self::ACTION_CLASS,
)
);
return $this;
}
/**
* Adds a condition to enable/disable the element.
*
@ -156,7 +195,7 @@ class DisplayRule {
public function action_enable( string $selector ): self {
$this->add_action(
array(
'type' => self::ACTION_TYPE_ELEMENT,
'type' => self::ACTION_TYPE_VISIBILITY,
'selector' => $selector,
'action' => self::ACTION_ENABLE,
)

View file

@ -270,6 +270,40 @@ class OrderProcessor {
do_action( 'woocommerce_paypal_payments_after_order_processor', $wc_order, $order );
}
/**
* Processes a given WooCommerce order and captured/authorizes the connected PayPal orders.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param Order $order The PayPal order.
*
* @throws Exception If processing fails.
*/
public function process_captured_and_authorized( WC_Order $wc_order, Order $order ): void {
$this->add_paypal_meta( $wc_order, $order, $this->environment );
if ( $order->intent() === 'AUTHORIZE' ) {
$wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' );
if ( $this->subscription_helper->has_subscription( $wc_order->get_id() ) ) {
$wc_order->update_meta_data( '_ppcp_captured_vault_webhook', 'false' );
}
}
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$this->update_transaction_id( $transaction_id, $wc_order );
}
$this->handle_new_order_status( $order, $wc_order );
if ( $this->capture_authorized_downloads( $order ) ) {
$this->authorized_payments_processor->capture_authorized_payment( $wc_order );
}
do_action( 'woocommerce_paypal_payments_after_order_processor', $wc_order, $order );
}
/**
* Creates a PayPal order for the given WC order.
*

View file

@ -436,7 +436,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'desc_tip' => true,
'label' => $container->get( 'wcgateway.settings.fraudnet-label' ),
'description' => __( 'FraudNet is a JavaScript library developed by PayPal and embedded into a merchants web page to collect browser-based data to help reduce fraud.', 'woocommerce-paypal-payments' ),
'default' => false,
'default' => true,
'screens' => array(
State::STATE_ONBOARDED,
),
@ -522,8 +522,8 @@ return function ( ContainerInterface $container, array $fields ): array {
'woocommerce-paypal-payments'
),
'options' => array(
PurchaseUnitSanitizer::MODE_DITCH => __( 'Do not send line items to PayPal', 'woocommerce-paypal-payments' ),
PurchaseUnitSanitizer::MODE_EXTRA_LINE => __( 'Add another line item', 'woocommerce-paypal-payments' ),
PurchaseUnitSanitizer::MODE_DITCH => __( 'Do not send line items to PayPal', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
@ -538,6 +538,7 @@ return function ( ContainerInterface $container, array $fields ): array {
->rule()
->condition_element( 'subtotal_mismatch_behavior', PurchaseUnitSanitizer::MODE_EXTRA_LINE )
->action_visible( 'subtotal_mismatch_line_name' )
->action_class( 'subtotal_mismatch_behavior', 'active' )
->to_array(),
)
),
@ -548,6 +549,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'type' => 'text',
'desc_tip' => true,
'description' => __( 'The name of the extra line that will be sent to PayPal to correct the subtotal mismatch.', 'woocommerce-paypal-payments' ),
'classes' => array( 'ppcp-field-indent' ),
'maxlength' => 22,
'default' => '',
'screens' => array(

View file

@ -91,7 +91,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'title' => __( 'Customize Smart Buttons Per Location', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Customize smart button style per location', 'woocommerce-paypal-payments' ),
'default' => true,
'default' => false,
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(),
'gateway' => 'paypal',
@ -126,7 +126,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'label' => __( 'Enable tagline', 'woocommerce-paypal-payments' ),
'desc_tip' => true,
'description' => __(
'Add the tagline. This line will only show up, if you select a horizontal layout.',
'Enable to show the tagline below the payment button. Requires button width of 300px minimum to appear.',
'woocommerce-paypal-payments'
),
'screens' => array(
@ -272,7 +272,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'label' => __( 'Enable tagline', 'woocommerce-paypal-payments' ),
'desc_tip' => true,
'description' => __(
'Add the tagline. This line will only show up, if you select a horizontal layout.',
'Enable to show the tagline below the payment button. Requires button width of 300px minimum to appear.',
'woocommerce-paypal-payments'
),
'screens' => array(
@ -412,7 +412,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'default' => false,
'desc_tip' => true,
'description' => __(
'Add the tagline. This line will only show up, if you select a horizontal layout.',
'Enable to show the tagline below the payment button. Requires button width of 300px minimum to appear.',
'woocommerce-paypal-payments'
),
'screens' => array(
@ -552,7 +552,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'default' => false,
'desc_tip' => true,
'description' => __(
'Add the tagline. This line will only show up, if you select a horizontal layout.',
'Enable to show the tagline below the payment button. Requires button width of 300px minimum to appear.',
'woocommerce-paypal-payments'
),
'screens' => array(
@ -692,7 +692,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'default' => false,
'desc_tip' => true,
'description' => __(
'Add the tagline. This line will only show up, if you select a horizontal layout.',
'Enable to show the tagline below the payment button. Requires button width of 300px minimum to appear.',
'woocommerce-paypal-payments'
),
'screens' => array(

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -37,6 +38,7 @@ trait PageMatcherTrait {
Settings::PAY_LATER_TAB_ID => Settings::PAY_LATER_TAB_ID,
CreditCardGateway::ID => 'dcc', // TODO: consider using just the gateway ID for PayPal and DCC too.
CardButtonGateway::ID => CardButtonGateway::ID,
AxoGateway::ID => 'axo',
);
return array_key_exists( $current_page_id, $gateway_page_id_map )
&& in_array( $gateway_page_id_map[ $current_page_id ], $allowed_gateways, true );

View file

@ -142,7 +142,7 @@ class Settings implements ContainerInterface {
'woocommerce-paypal-payments'
),
'smart_button_locations' => $this->default_button_locations,
'smart_button_enable_styling_per_location' => true,
'smart_button_enable_styling_per_location' => false,
'pay_later_messaging_enabled' => true,
'pay_later_button_enabled' => true,
'pay_later_button_locations' => $this->default_pay_later_button_locations,

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