Resolve merge conflict

This commit is contained in:
Daniel Dudzic 2024-04-20 19:38:41 +02:00
commit 2317c90db4
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
77 changed files with 6249 additions and 123 deletions

View file

@ -81,5 +81,12 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-paylater-configurator/module.php" )(); $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; return $modules;
}; };

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient; namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
@ -1633,4 +1634,11 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $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

@ -115,7 +115,7 @@ class PaymentMethodTokensEndpoint {
* @throws RuntimeException When something when wrong with the request. * @throws RuntimeException When something when wrong with the request.
* @throws PayPalApiException When something when wrong setting up the token. * @throws PayPalApiException When something when wrong setting up the token.
*/ */
public function payment_tokens( PaymentSource $payment_source ): stdClass { public function create_payment_token( PaymentSource $payment_source ): stdClass {
$data = array( $data = array(
'payment_source' => array( 'payment_source' => array(
$payment_source->name() => $payment_source->properties(), $payment_source->name() => $payment_source->properties(),

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Exception; namespace WooCommerce\PayPalCommerce\ApiClient\Exception;
use stdClass;
/** /**
* Class PayPalApiException * Class PayPalApiException
*/ */
@ -17,7 +19,7 @@ class PayPalApiException extends RuntimeException {
/** /**
* The JSON response object of PayPal. * The JSON response object of PayPal.
* *
* @var \stdClass * @var stdClass
*/ */
private $response; private $response;
@ -31,10 +33,10 @@ class PayPalApiException extends RuntimeException {
/** /**
* PayPalApiException constructor. * PayPalApiException constructor.
* *
* @param \stdClass|null $response The JSON object. * @param stdClass|null $response The JSON object.
* @param int $status_code The HTTP status code. * @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 ) ) { if ( is_null( $response ) ) {
$response = new \stdClass(); $response = new \stdClass();
} }
@ -65,7 +67,7 @@ class PayPalApiException extends RuntimeException {
*/ */
$this->response = $response; $this->response = $response;
$this->status_code = $status_code; $this->status_code = $status_code;
$message = $response->message; $message = $this->get_customer_friendly_message( $response );
if ( $response->name ) { if ( $response->name ) {
$message = '[' . $response->name . '] ' . $message; $message = '[' . $response->name . '] ' . $message;
} }
@ -141,4 +143,40 @@ class PayPalApiException extends RuntimeException {
return $details; 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' ) ? $payment_preference = $this->settings->has( 'payee_preferred' ) && $this->settings->get( 'payee_preferred' ) ?
ApplicationContext::PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED : ApplicationContext::PAYMENT_METHOD_UNRESTRICTED; ApplicationContext::PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED : ApplicationContext::PAYMENT_METHOD_UNRESTRICTED;
$context = new ApplicationContext( $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) wc_get_checkout_url(),
(string) $brand_name, (string) $brand_name,
$locale, $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' ); $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. // Connection tab fields.
$fields = $insert_after( $fields = $insert_after(
$fields, $fields,
@ -91,10 +93,15 @@ return array(
$connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">'; $connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">';
return $insert_after( return $insert_after(
$fields, $fields,
'allow_card_button_gateway', 'digital_wallet_heading',
array( array(
'applepay_button_enabled' => array( 'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ), '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', 'type' => 'checkbox',
'class' => array( 'ppcp-grayed-out-text' ), 'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => array( 'ppcp-disabled-checkbox' ), 'input_class' => array( 'ppcp-disabled-checkbox' ),
@ -109,7 +116,7 @@ return array(
. '</p>', . '</p>',
'default' => 'yes', 'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array( 'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode( '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( return $insert_after(
$fields, $fields,
'allow_card_button_gateway', 'digital_wallet_heading',
array( 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( 'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ), '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', 'type' => 'checkbox',
'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' ) 'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">' . '<p class="description">'
@ -145,7 +168,7 @@ return array(
. '</p>', . '</p>',
'default' => 'yes', 'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array( 'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode( 'data-ppcp-display' => wp_json_encode(
@ -160,10 +183,12 @@ return array(
->action_visible( 'applepay_button_type' ) ->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' ) ->action_visible( 'applepay_button_language' )
->action_visible( 'applepay_checkout_data_mode' ) ->action_visible( 'applepay_checkout_data_mode' )
->action_class( 'applepay_button_enabled', 'active' )
->to_array(), ->to_array(),
) )
), ),
), ),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
), ),
'applepay_button_domain_registration' => array( 'applepay_button_domain_registration' => array(
'title' => __( 'Domain Registration', 'woocommerce-paypal-payments' ), 'title' => __( 'Domain Registration', 'woocommerce-paypal-payments' ),
@ -183,7 +208,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_button_domain_validation' => array( 'applepay_button_domain_validation' => array(
@ -206,7 +231,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_button_device_eligibility' => array( 'applepay_button_device_eligibility' => array(
@ -224,7 +249,7 @@ return array(
'classes' => array( 'ppcp-field-indent' ), 'classes' => array( 'ppcp-field-indent' ),
'class' => array(), 'class' => array(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_button_type' => array( 'applepay_button_type' => array(
@ -241,7 +266,7 @@ return array(
'default' => 'pay', 'default' => 'pay',
'options' => PropertiesDictionary::button_types(), 'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_button_color' => array( 'applepay_button_color' => array(
@ -259,7 +284,7 @@ return array(
'default' => 'black', 'default' => 'black',
'options' => PropertiesDictionary::button_colors(), 'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_button_language' => array( 'applepay_button_language' => array(
@ -276,7 +301,7 @@ return array(
'default' => 'en', 'default' => 'en',
'options' => PropertiesDictionary::button_languages(), 'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'applepay_checkout_data_mode' => array( 'applepay_checkout_data_mode' => array(
@ -290,7 +315,7 @@ return array(
'default' => PropertiesDictionary::BILLING_DATA_MODE_DEFAULT, 'default' => PropertiesDictionary::BILLING_DATA_MODE_DEFAULT,
'options' => PropertiesDictionary::billing_data_modes(), 'options' => PropertiesDictionary::billing_data_modes(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
) )

View file

@ -39,7 +39,7 @@ class ApplepayButton {
this.log = function() { this.log = function() {
if ( this.buttonConfig.is_debug ) { 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,336 @@
<?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',
),
'gateway' => 'dcc',
),
'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' => 'dcc',
'requirements' => array(),
'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_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_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' => __(
'Fastlane Debit & Credit Cards',
'woocommerce-paypal-payments'
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_privacy' => array(
'title' => __( 'Privacy', 'woocommerce-paypal-payments' ),
'type' => 'select',
'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' => 'dcc',
'requirements' => array(),
),
'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',
),
'gateway' => 'dcc',
),
'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' ),
'gateway' => 'dcc',
),
'axo_style_root_bg_color' => array(
'title' => __( 'Background Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_root_error_color' => array(
'title' => __( 'Error Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_root_font_family' => array(
'title' => __( 'Font Family', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_root_font_size_base' => array(
'title' => __( 'Font Size Base', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_root_padding' => array(
'title' => __( 'Padding', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_root_primary_color' => array(
'title' => __( 'Primary Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'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' ),
'gateway' => 'dcc',
),
'axo_style_input_bg_color' => array(
'title' => __( 'Background Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_input_border_radius' => array(
'title' => __( 'Border Radius', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_input_border_color' => array(
'title' => __( 'Border Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_input_border_width' => array(
'title' => __( 'Border Width', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_input_text_color_base' => array(
'title' => __( 'Text Color Base', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
'axo_style_input_focus_border_color' => array(
'title' => __( 'Focus Border Color', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
),
)
);
},
);

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,40 @@
.ppcp-axo-card-icons {
padding: 4px 0 16px 25px;
.ppcp-card-icon {
float: left !important;
}
}
.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;
}

View file

@ -0,0 +1,733 @@
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";
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();
});
}
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.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)));
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);
emailRow.querySelector('input').focus();
}
}
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() {
(await this.fastlane.FastlaneWatermarkComponent({
includeAdditionalInfo: true
})).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'
});
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));
// Add addresses
this.setShipping(authResponse.profileData.shippingAddress);
this.setBilling({
address: authResponse.profileData.card.paymentSource.card.billingAddress
});
this.setCard(authResponse.profileData.card);
this.setStatus('validEmail', true);
this.setStatus('hasProfile', true);
this.hideGatewaySelection = true;
this.$('.wc_payment_methods label').hide();
this.rerender();
} else {
// authentication failed or canceled by the customer
log("Authentication Failed")
}
} 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);
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.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() {
return {
fields: {
cardholderName: {} // optionally pass this to show the card holder name
}
}
}
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]);
});
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';
}
}
export default AxoManager;

View file

@ -0,0 +1,41 @@
import {setVisible} from "../../../../ppcp-button/resources/js/modules/Helper/Hiding";
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,41 @@
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) => {
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,126 @@
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;">
<h3>Billing <a href="javascript:void(0)" ${this.el.changeBillingAddressLink.attributes} style="margin-left: 20px;">Edit</a></h3>
<div>Please fill in your billing details.</div>
</div>
`;
}
return `
<div style="margin-bottom: 20px;">
<h3>Billing <a href="javascript:void(0)" ${this.el.changeBillingAddressLink.attributes} style="margin-left: 20px;">Edit</a></h3>
<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,
}
}
});
}
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,114 @@
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;">
<h3>Card Details <a href="javascript:void(0)" ${this.el.changeCardLink.attributes} style="margin-left: 20px;">Edit</a></h3>
<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,134 @@
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;">
<h3>Shipping <a href="javascript:void(0)" ${this.el.changeShippingAddressLink.attributes} style="margin-left: 20px;">Edit</a></h3>
<div>Please fill in your shipping details.</div>
</div>
`;
}
return `
<div style="margin-bottom: 20px;">
<h3>Shipping <a href="javascript:void(0)" ${this.el.changeShippingAddressLink.attributes} style="margin-left: 20px;">Edit</a></h3>
<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('#shipping_state', data.value('stateCode'))}</div>
<div>${valueOfSelect('#shipping_country', data.value('countryCode'))}</div>
<div>${data.value('phone')}</div>
</div>
`;
},
fields: {
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,117 @@
<?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' ),
$container->get( 'wcgateway.url' ),
$container->get( 'wcgateway.order-processor' ),
$container->get( 'axo.card_icons' ),
$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',
),
);
},
/**
* 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,210 @@
<?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' ),
),
),
);
}
/**
* 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,224 @@
<?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;
/**
* 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
);
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() {
}
}

View file

@ -0,0 +1,324 @@
<?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\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
/**
* Class AXOGateway.
*/
class AxoGateway extends WC_Payment_Gateway {
use OrderMetaTrait;
const ID = 'ppcp-axo-gateway';
/**
* 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 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 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 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(
ContainerInterface $ppcp_settings,
string $wcgateway_module_url,
OrderProcessor $order_processor,
array $card_icons,
OrderEndpoint $order_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
ShippingPreferenceFactory $shipping_preference_factory,
TransactionUrlProvider $transaction_url_provider,
Environment $environment,
LoggerInterface $logger
) {
$this->id = self::ID;
$this->ppcp_settings = $ppcp_settings;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->order_processor = $order_processor;
$this->card_icons = $card_icons;
$this->method_title = __( 'Fastlane Debit & Credit Cards', 'woocommerce-paypal-payments' );
$this->method_description = __( 'Accept credit cards with Fastlanes latest solution.', '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' ),
),
);
}
/**
* 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 );
$payment_method_title = __( 'Credit Card (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();
if ( empty( $this->card_icons ) ) {
return $icon;
}
$images = array();
foreach ( $this->card_icons as $card ) {
$images[] = '<img
class="ppcp-card-icon"
title="' . $card['title'] . '"
src="' . esc_url( $this->wcgateway_module_url ) . 'assets/images/' . $card['file'] . '"
> ';
}
return '<div class="ppcp-axo-card-icons">' . implode( '', $images ) . '</div>';
}
/**
* 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();
}
}

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

View file

@ -121,8 +121,13 @@ const PayPalComponent = ({
}; };
const createSubscription = async (data, actions) => { 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({ return actions.subscription.create({
'plan_id': config.scriptData.subscription_plan_id 'plan_id': planId
}); });
}; };

View file

@ -137,7 +137,12 @@ const bootstrap = () => {
} }
const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart;
if (isFreeTrial && data.fundingSource !== 'card' && ! PayPalCommerceGateway.subscription_plan_id) { if (
isFreeTrial
&& data.fundingSource !== 'card'
&& ! PayPalCommerceGateway.subscription_plan_id
&& ! PayPalCommerceGateway.vault_v3_enabled
) {
freeTrialHandler.handle(); freeTrialHandler.handle();
return actions.reject(); return actions.reject();
} }

View file

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

View file

@ -12,7 +12,7 @@ class CheckoutActionHandler {
this.spinner = spinner; this.spinner = spinner;
} }
subscriptionsConfiguration() { subscriptionsConfiguration(subscription_plan_id) {
return { return {
createSubscription: async (data, actions) => { createSubscription: async (data, actions) => {
try { try {
@ -22,7 +22,7 @@ class CheckoutActionHandler {
} }
return actions.subscription.create({ return actions.subscription.create({
'plan_id': this.config.subscription_plan_id 'plan_id': subscription_plan_id
}); });
}, },
onApprove: (data, actions) => { onApprove: (data, actions) => {
@ -144,6 +144,54 @@ class CheckoutActionHandler {
} }
} }
} }
addPaymentMethodConfiguration() {
return {
createVaultSetupToken: async () => {
const response = await fetch(this.config.ajax.create_setup_token.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: this.config.ajax.create_setup_token.nonce,
})
});
const result = await response.json()
if (result.data.id) {
return result.data.id
}
console.error(result)
},
onApprove: async ({vaultSetupToken}) => {
const response = await fetch(this.config.ajax.create_payment_token_for_guest.endpoint, {
method: "POST",
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nonce: this.config.ajax.create_payment_token_for_guest.nonce,
vault_setup_token: vaultSetupToken,
})
})
const result = await response.json();
if (result.success === true) {
document.querySelector('#place_order').click()
return;
}
console.error(result)
},
onError: (error) => {
console.error(error)
}
}
}
} }
export default CheckoutActionHandler; export default CheckoutActionHandler;

View file

@ -90,7 +90,12 @@ class CartBootstrap {
PayPalCommerceGateway.data_client_id.has_subscriptions PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled && 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) { if(!PayPalCommerceGateway.subscription_product_allowed) {
this.gateway.button.is_disabled = true; this.gateway.button.is_disabled = true;

View file

@ -106,7 +106,11 @@ class CheckoutBootstap {
PayPalCommerceGateway.data_client_id.has_subscriptions PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled && 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) { if(!PayPalCommerceGateway.subscription_product_allowed) {
this.gateway.button.is_disabled = true; this.gateway.button.is_disabled = true;
@ -116,6 +120,14 @@ class CheckoutBootstap {
return; return;
} }
if(
PayPalCommerceGateway.is_free_trial_cart
&& PayPalCommerceGateway.vault_v3_enabled
) {
this.renderer.render(actionHandler.addPaymentMethodConfiguration(), {}, actionHandler.configuration());
return;
}
this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration()); this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration());
} }

View file

@ -60,6 +60,13 @@ export const loadPaypalScript = (config, onLoaded, onError = null) => {
scriptOptions = merge(scriptOptions, config.script_attributes); 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 // Load PayPal script for special case with data-client-token
if (config.data_client_id?.set_attribute) { if (config.data_client_id?.set_attribute) {
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback); dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback);

View file

@ -145,6 +145,8 @@ return array(
$container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ), $container->get( 'button.pay-now-contexts' ),
$container->get( 'wcgateway.funding-sources-without-redirect' ), $container->get( 'wcgateway.funding-sources-without-redirect' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },

View file

@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WC_Product; use WC_Product;
use WC_Product_Variation; use WC_Product_Variation;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
@ -34,6 +35,9 @@ use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule; use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule;
use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksModule; use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksModule;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
@ -185,13 +189,6 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $funding_sources_without_redirect; private $funding_sources_without_redirect;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/** /**
* Session handler. * Session handler.
* *
@ -199,6 +196,27 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $session_handler; private $session_handler;
/**
* Whether Vault v3 module is enabled.
*
* @var bool
*/
private $vault_v3_enabled;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/** /**
* SmartButton constructor. * SmartButton constructor.
* *
@ -221,6 +239,8 @@ class SmartButton implements SmartButtonInterface {
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param array $pay_now_contexts The contexts that should have the Pay Now button. * @param array $pay_now_contexts The contexts that should have the Pay Now button.
* @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. * @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back.
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
@ -243,6 +263,8 @@ class SmartButton implements SmartButtonInterface {
bool $early_validation_enabled, bool $early_validation_enabled,
array $pay_now_contexts, array $pay_now_contexts,
array $funding_sources_without_redirect, array $funding_sources_without_redirect,
bool $vault_v3_enabled,
PaymentTokensEndpoint $payment_tokens_endpoint,
LoggerInterface $logger LoggerInterface $logger
) { ) {
@ -265,7 +287,9 @@ class SmartButton implements SmartButtonInterface {
$this->early_validation_enabled = $early_validation_enabled; $this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts; $this->pay_now_contexts = $pay_now_contexts;
$this->funding_sources_without_redirect = $funding_sources_without_redirect; $this->funding_sources_without_redirect = $funding_sources_without_redirect;
$this->vault_v3_enabled = $vault_v3_enabled;
$this->logger = $logger; $this->logger = $logger;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
} }
/** /**
@ -1062,45 +1086,59 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'redirect' => wc_get_checkout_url(), 'redirect' => wc_get_checkout_url(),
'context' => $this->context(), 'context' => $this->context(),
'ajax' => array( 'ajax' => array(
'simulate_cart' => array( 'simulate_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ), 'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ),
), ),
'change_cart' => array( 'change_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
), ),
'create_order' => array( 'create_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ),
), ),
'approve_order' => array( 'approve_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ),
), ),
'approve_subscription' => array( 'approve_subscription' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ), 'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ),
), ),
'vault_paypal' => array( 'vault_paypal' => array(
'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ),
), ),
'save_checkout_form' => array( 'save_checkout_form' => array(
'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ), 'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ),
), ),
'validate_checkout' => array( 'validate_checkout' => array(
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
), ),
'cart_script_params' => array( 'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
), ),
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),
),
'create_payment_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ),
),
'create_payment_token_for_guest' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentTokenForGuest::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentTokenForGuest::nonce() ),
),
), ),
'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(), 'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(),
'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(), '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_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(), 'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(),
'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(), 'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(),
'enforce_vault' => $this->has_subscriptions(), 'enforce_vault' => $this->has_subscriptions(),
@ -1931,8 +1969,18 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
*/ */
private function get_vaulted_paypal_email(): string { private function get_vaulted_paypal_email(): string {
try { try {
$tokens = $this->get_payment_tokens(); $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( $customer_id ) {
$customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id );
foreach ( $customer_tokens as $token ) {
$email_address = $token['payment_source']->properties()->email_address ?? '';
if ( $email_address ) {
return $email_address;
}
}
}
$tokens = $this->get_payment_tokens();
foreach ( $tokens as $token ) { foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) { if ( isset( $token->source()->paypal ) ) {
return $token->source()->paypal->payer->email_address; return $token->source()->paypal->payer->email_address;
@ -1941,6 +1989,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
} catch ( Exception $exception ) { } catch ( Exception $exception ) {
$this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() ); $this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() );
} }
return ''; 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' ); $display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager ); assert( $display_manager instanceof DisplayManager );
$module_url = $container->get( 'googlepay.url' );
// Connection tab fields. // Connection tab fields.
$fields = $insert_after( $fields = $insert_after(
$fields, $fields,
@ -62,10 +64,15 @@ return array(
$connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">'; $connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">';
return $insert_after( return $insert_after(
$fields, $fields,
'allow_card_button_gateway', 'digital_wallet_heading',
array( array(
'googlepay_button_enabled' => array( 'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ), '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', 'type' => 'checkbox',
'class' => array( 'ppcp-grayed-out-text' ), 'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => array( 'ppcp-disabled-checkbox' ), 'input_class' => array( 'ppcp-disabled-checkbox' ),
@ -80,7 +87,7 @@ return array(
. '</p>', . '</p>',
'default' => 'yes', 'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array( 'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode( '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. // Standard Payments tab fields.
return $insert_after( return $insert_after(
$fields, $fields,
'allow_card_button_gateway', 'digital_wallet_heading',
array( array(
'googlepay_button_enabled' => array( 'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ), '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', 'type' => 'checkbox',
'label' => __( 'Enable Google Pay button', 'woocommerce-paypal-payments' ) 'label' => __( 'Enable Google Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">' . '<p class="description">'
@ -117,7 +130,7 @@ return array(
. '</p>', . '</p>',
'default' => 'yes', 'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
'custom_attributes' => array( 'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode( 'data-ppcp-display' => wp_json_encode(
@ -129,10 +142,12 @@ return array(
->action_visible( 'googlepay_button_color' ) ->action_visible( 'googlepay_button_color' )
->action_visible( 'googlepay_button_language' ) ->action_visible( 'googlepay_button_language' )
->action_visible( 'googlepay_button_shipping_enabled' ) ->action_visible( 'googlepay_button_shipping_enabled' )
->action_class( 'googlepay_button_enabled', 'active' )
->to_array(), ->to_array(),
) )
), ),
), ),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
), ),
'googlepay_button_type' => array( 'googlepay_button_type' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ), 'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
@ -148,7 +163,7 @@ return array(
'default' => 'pay', 'default' => 'pay',
'options' => PropertiesDictionary::button_types(), 'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'googlepay_button_color' => array( 'googlepay_button_color' => array(
@ -166,7 +181,7 @@ return array(
'default' => 'black', 'default' => 'black',
'options' => PropertiesDictionary::button_colors(), 'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'googlepay_button_language' => array( 'googlepay_button_language' => array(
@ -183,7 +198,7 @@ return array(
'default' => 'en', 'default' => 'en',
'options' => PropertiesDictionary::button_languages(), 'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
'googlepay_button_shipping_enabled' => array( 'googlepay_button_shipping_enabled' => array(
@ -198,7 +213,7 @@ return array(
'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ), 'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ),
'default' => 'no', 'default' => 'no',
'screens' => array( State::STATE_ONBOARDED ), 'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal', 'gateway' => 'dcc',
'requirements' => array(), 'requirements' => array(),
), ),
) )

View file

@ -28,7 +28,7 @@ class GooglepayButton {
this.log = function() { this.log = function() {
if ( this.buttonConfig.is_debug ) { 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' ) __( '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> <li>
<label> <label>
<input type="radio" id="ppcp-onboarding-dcc-basic" name="ppcp_onboarding_dcc" value="basic" checked ' . <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' ) __( '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> <li>
<label> <label>
<input type="radio" id="ppcp-onboarding-dcc-acdc" name="ppcp_onboarding_dcc" value="acdc" ' . <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. * @param string $note The additional description text, such as about conditions.
* @return string * @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; $value_html = $value;
if ( $note ) { if ( $note ) {
$value_html .= '<br/><span class="ppcp-muted-text">' . $note . '</span>'; $value_html .= '<br/><span class="ppcp-muted-text">' . $note . '</span>';

View file

@ -12,7 +12,7 @@ import ErrorHandler from "../../../ppcp-button/resources/js/modules/ErrorHandler
import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper"; import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper";
const errorHandler = new ErrorHandler( const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic, ppcp_add_payment_method.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper') document.querySelector('.woocommerce-notices-wrapper')
); );

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsApplies; use WooCommerce\PayPalCommerce\SavePaymentMethods\Helper\SavePaymentMethodsApplies;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -811,4 +812,10 @@ return array(
$container->get( 'vaulting.wc-payment-tokens' ) $container->get( 'vaulting.wc-payment-tokens' )
); );
}, },
'save-payment-methods.endpoint.create-payment-token-for-guest' => static function ( ContainerInterface $container ): CreatePaymentTokenForGuest {
return new CreatePaymentTokenForGuest(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.payment-method-tokens' )
);
},
); );

View file

@ -94,7 +94,7 @@ class CreatePaymentToken implements EndpointInterface {
) )
); );
$result = $this->payment_method_tokens_endpoint->payment_tokens( $payment_source ); $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source );
if ( is_user_logged_in() && isset( $result->customer->id ) ) { if ( is_user_logged_in() && isset( $result->customer->id ) ) {
$current_user_id = get_current_user_id(); $current_user_id = get_current_user_id();

View file

@ -0,0 +1,90 @@
<?php
/**
* Create payment token for guest user.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* Class UpdateCustomerId
*/
class CreatePaymentTokenForGuest implements EndpointInterface {
const ENDPOINT = 'ppc-update-customer-id';
/**
* The request data.
*
* @var RequestData
*/
private $request_data;
/**
* The payment method tokens endpoint.
*
* @var PaymentMethodTokensEndpoint
*/
private $payment_method_tokens_endpoint;
/**
* CreatePaymentToken constructor.
*
* @param RequestData $request_data The request data.
* @param PaymentMethodTokensEndpoint $payment_method_tokens_endpoint The payment method tokens endpoint.
*/
public function __construct(
RequestData $request_data,
PaymentMethodTokensEndpoint $payment_method_tokens_endpoint
) {
$this->request_data = $request_data;
$this->payment_method_tokens_endpoint = $payment_method_tokens_endpoint;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws Exception On Error.
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
/**
* Suppress ArgumentTypeCoercion
*
* @psalm-suppress ArgumentTypeCoercion
*/
$payment_source = new PaymentSource(
'token',
(object) array(
'id' => $data['vault_setup_token'],
'type' => 'SETUP_TOKEN',
)
);
$result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source );
WC()->session->set( 'ppcp_guest_payment_for_free_trial', $result );
wp_send_json_success();
return true;
}
}

View file

@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
@ -316,6 +317,14 @@ class SavePaymentMethodsModule implements ModuleInterface {
'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ), 'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ),
), ),
), ),
'labels' => array(
'error' => array(
'generic' => __(
'Something went wrong. Please try again or choose another payment source.',
'woocommerce-paypal-payments'
),
),
),
) )
); );
} catch ( RuntimeException $exception ) { } catch ( RuntimeException $exception ) {
@ -363,6 +372,16 @@ class SavePaymentMethodsModule implements ModuleInterface {
} }
); );
add_action(
'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' );
assert( $endpoint instanceof CreatePaymentTokenForGuest );
$endpoint->handle_request();
}
);
add_action( add_action(
'woocommerce_paypal_payments_before_delete_payment_token', 'woocommerce_paypal_payments_before_delete_payment_token',
function( string $token_id ) use ( $c ) { function( string $token_id ) use ( $c ) {

View file

@ -64,4 +64,7 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'vaulting.vault-v3-enabled' => static function( ContainerInterface $container ): bool {
return $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' );
},
); );

View file

@ -1,5 +1,8 @@
@use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button; @use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button;
$border-color: #c3c3c3;
$background-ident-color: #fbfbfb;
.ppcp-field-hidden { .ppcp-field-hidden {
display: none !important; display: none !important;
} }
@ -11,9 +14,10 @@
opacity: 0.5; opacity: 0.5;
} }
.ppcp-field-indent { .ppcp-active-spacer {
th { th, td {
padding-left: 20px; padding: 0;
height: 1rem;
} }
} }
@ -39,3 +43,51 @@
font-weight: bold; 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;
}
}
}
}
}

View file

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

View file

@ -1,5 +1,6 @@
import ElementCondition from "./condition/ElementCondition"; import ElementCondition from "./condition/ElementCondition";
import BoolCondition from "./condition/BoolCondition"; import BoolCondition from "./condition/BoolCondition";
import JsVariableCondition from "./condition/JsVariableCondition";
class ConditionFactory { class ConditionFactory {
static make(conditionConfig, triggerUpdate) { static make(conditionConfig, triggerUpdate) {
@ -8,6 +9,8 @@ class ConditionFactory {
return new ElementCondition(conditionConfig, triggerUpdate); return new ElementCondition(conditionConfig, triggerUpdate);
case 'bool': case 'bool':
return new BoolCondition(conditionConfig, triggerUpdate); return new BoolCondition(conditionConfig, triggerUpdate);
case 'js_variable':
return new JsVariableCondition(conditionConfig, triggerUpdate);
} }
throw new Error('[ConditionFactory] Unknown condition: ' + conditionConfig.type); 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"; import BaseAction from "./BaseAction";
class ElementAction extends BaseAction { class VisibilityAction extends BaseAction {
run(status) { 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\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers; use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
@ -103,7 +104,10 @@ return array(
$api_shop_country, $api_shop_country,
$container->get( 'api.endpoint.order' ), $container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.paypal-checkout-url' ), $container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.place-order-button-text' ) $container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'vaulting.wc-payment-tokens' )
); );
}, },
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
@ -806,25 +810,6 @@ return array(
), ),
'gateway' => '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' ),
),
'3d_secure_heading' => array( '3d_secure_heading' => array(
'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ), 'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading', 'type' => 'ppcp-heading',
@ -881,6 +866,52 @@ return array(
), ),
'gateway' => 'dcc', '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( 'paypal_saved_payments' => array(
'heading' => __( 'Saved payments', 'woocommerce-paypal-payments' ), 'heading' => __( 'Saved payments', 'woocommerce-paypal-payments' ),
'description' => sprintf( 'description' => sprintf(
@ -919,6 +950,32 @@ return array(
'gateway' => 'paypal', 'gateway' => 'paypal',
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ), '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() ) { if ( ! $subscription_helper->plugin_is_active() ) {
@ -1506,6 +1563,7 @@ return array(
PayUponInvoiceGateway::ID, PayUponInvoiceGateway::ID,
CardButtonGateway::ID, CardButtonGateway::ID,
OXXOGateway::ID, OXXOGateway::ID,
AxoGateway::ID,
); );
}, },
'wcgateway.gateway-repository' => static function ( ContainerInterface $container ): GatewayRepository { 'wcgateway.gateway-repository' => static function ( ContainerInterface $container ): GatewayRepository {

View file

@ -33,6 +33,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/** /**
@ -40,7 +41,7 @@ use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
*/ */
class CreditCardGateway extends \WC_Payment_Gateway_CC { class CreditCardGateway extends \WC_Payment_Gateway_CC {
use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait; use ProcessPaymentTrait, GatewaySettingsRendererTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait, FreeTrialHandlerTrait;
const ID = 'ppcp-credit-card-gateway'; const ID = 'ppcp-credit-card-gateway';
@ -454,6 +455,17 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
// phpcs:ignore WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.NonceVerification.Missing
$card_payment_token_id = wc_clean( wp_unslash( $_POST['wc-ppcp-credit-card-gateway-payment-token'] ?? '' ) ); $card_payment_token_id = wc_clean( wp_unslash( $_POST['wc-ppcp-credit-card-gateway-payment-token'] ?? '' ) );
if ( $this->is_free_trial_order( $wc_order ) && $card_payment_token_id ) {
$customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() );
foreach ( $customer_tokens as $token ) {
if ( $token['payment_source']->name() === 'card' ) {
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
}
if ( $card_payment_token_id ) { if ( $card_payment_token_id ) {
$customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() ); $customer_tokens = $this->wc_payment_tokens->customer_tokens( get_current_user_id() );

View file

@ -14,12 +14,14 @@ use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WC_Payment_Tokens; use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
@ -179,26 +181,50 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/ */
private $paypal_checkout_url_factory; private $paypal_checkout_url_factory;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* Whether Vault v3 module is enabled.
*
* @var bool
*/
private $vault_v3_enabled;
/**
* WooCommerce payment tokens.
*
* @var WooCommercePaymentTokens
*/
private $wc_payment_tokens;
/** /**
* PayPalGateway constructor. * PayPalGateway constructor.
* *
* @param SettingsRenderer $settings_renderer The Settings Renderer. * @param SettingsRenderer $settings_renderer The Settings Renderer.
* @param FundingSourceRenderer $funding_source_renderer The funding source renderer. * @param FundingSourceRenderer $funding_source_renderer The funding source renderer.
* @param OrderProcessor $order_processor The Order Processor. * @param OrderProcessor $order_processor The Order Processor.
* @param ContainerInterface $config The settings. * @param ContainerInterface $config The settings.
* @param SessionHandler $session_handler The Session Handler. * @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The Refund Processor. * @param RefundProcessor $refund_processor The Refund Processor.
* @param State $state The state. * @param State $state The state.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order. * @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper. * @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
* @param Environment $environment The environment. * @param Environment $environment The environment.
* @param PaymentTokenRepository $payment_token_repository The payment token repository. * @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param string $api_shop_country The api shop country. * @param string $api_shop_country The api shop country.
* @param OrderEndpoint $order_endpoint The order endpoint. * @param OrderEndpoint $order_endpoint The order endpoint.
* @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID. * @param callable(string):string $paypal_checkout_url_factory The function return the PayPal checkout URL for the given order ID.
* @param string $place_order_button_text The text for the standard "Place order" button. * @param string $place_order_button_text The text for the standard "Place order" button.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens.
*/ */
public function __construct( public function __construct(
SettingsRenderer $settings_renderer, SettingsRenderer $settings_renderer,
@ -217,7 +243,10 @@ class PayPalGateway extends \WC_Payment_Gateway {
string $api_shop_country, string $api_shop_country,
OrderEndpoint $order_endpoint, OrderEndpoint $order_endpoint,
callable $paypal_checkout_url_factory, callable $paypal_checkout_url_factory,
string $place_order_button_text string $place_order_button_text,
PaymentTokensEndpoint $payment_tokens_endpoint,
bool $vault_v3_enabled,
WooCommercePaymentTokens $wc_payment_tokens
) { ) {
$this->id = self::ID; $this->id = self::ID;
$this->settings_renderer = $settings_renderer; $this->settings_renderer = $settings_renderer;
@ -237,6 +266,10 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->api_shop_country = $api_shop_country; $this->api_shop_country = $api_shop_country;
$this->paypal_checkout_url_factory = $paypal_checkout_url_factory; $this->paypal_checkout_url_factory = $paypal_checkout_url_factory;
$this->order_button_text = $place_order_button_text; $this->order_button_text = $place_order_button_text;
$this->order_endpoint = $order_endpoint;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->vault_v3_enabled = $vault_v3_enabled;
$this->wc_payment_tokens = $wc_payment_tokens;
if ( $this->onboarded ) { if ( $this->onboarded ) {
$this->supports = array( 'refunds', 'tokenization' ); $this->supports = array( 'refunds', 'tokenization' );
@ -265,6 +298,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
'subscription_payment_method_change_admin', 'subscription_payment_method_change_admin',
'multiple_subscriptions' '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' ) ) { } elseif ( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) ) {
$this->supports[] = 'tokenization'; $this->supports[] = 'tokenization';
} }
@ -299,8 +334,6 @@ class PayPalGateway extends \WC_Payment_Gateway {
'process_admin_options', 'process_admin_options',
) )
); );
$this->order_endpoint = $order_endpoint;
} }
/** /**
@ -500,7 +533,49 @@ class PayPalGateway extends \WC_Payment_Gateway {
$wc_order->save(); $wc_order->save();
} }
if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) && ! $this->subscription_helper->paypal_subscription_id() ) { if (
'card' !== $funding_source
&& $this->is_free_trial_order( $wc_order )
&& ! $this->subscription_helper->paypal_subscription_id()
) {
$ppcp_guest_payment_for_free_trial = WC()->session->get( 'ppcp_guest_payment_for_free_trial' ) ?? null;
if ( $this->vault_v3_enabled && $ppcp_guest_payment_for_free_trial ) {
$customer_id = $ppcp_guest_payment_for_free_trial->customer->id ?? '';
if ( $customer_id ) {
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
}
if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal ) ) {
$email = '';
if ( isset( $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address ) ) {
$email = $ppcp_guest_payment_for_free_trial->payment_source->paypal->email_address;
}
$this->wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$ppcp_guest_payment_for_free_trial->id,
$email
);
}
WC()->session->set( 'ppcp_guest_payment_for_free_trial', null );
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
$customer_id = get_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', true );
if ( $customer_id ) {
$customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id );
foreach ( $customer_tokens as $token ) {
$payment_source_name = $token['payment_source']->name() ?? '';
if ( $payment_source_name === 'paypal' || $payment_source_name === 'venmo' ) {
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
}
}
$user_id = (int) $wc_order->get_customer_id(); $user_id = (int) $wc_order->get_customer_id();
$tokens = $this->payment_token_repository->all_for_user_id( $user_id ); $tokens = $this->payment_token_repository->all_for_user_id( $user_id );
if ( ! array_filter( if ( ! array_filter(
@ -513,7 +588,6 @@ class PayPalGateway extends \WC_Payment_Gateway {
} }
$wc_order->payment_complete(); $wc_order->payment_complete();
return $this->handle_payment_success( $wc_order ); return $this->handle_payment_success( $wc_order );
} }

View file

@ -16,8 +16,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/ */
class DisplayRule { class DisplayRule {
const CONDITION_TYPE_ELEMENT = 'element'; const CONDITION_TYPE_ELEMENT = 'element';
const CONDITION_TYPE_BOOL = 'bool'; const CONDITION_TYPE_BOOL = 'bool';
const CONDITION_TYPE_JS_VARIABLE = 'js_variable';
const CONDITION_OPERATION_EQUALS = 'equals'; const CONDITION_OPERATION_EQUALS = 'equals';
const CONDITION_OPERATION_NOT_EQUALS = 'not_equals'; const CONDITION_OPERATION_NOT_EQUALS = 'not_equals';
@ -26,10 +27,12 @@ class DisplayRule {
const CONDITION_OPERATION_EMPTY = 'empty'; const CONDITION_OPERATION_EMPTY = 'empty';
const CONDITION_OPERATION_NOT_EMPTY = 'not_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_VISIBLE = 'visible';
const ACTION_ENABLE = 'enable'; const ACTION_ENABLE = 'enable';
const ACTION_CLASS = 'class';
/** /**
* The element selector. * The element selector.
@ -132,6 +135,24 @@ class DisplayRule {
return $this; 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. * Adds a condition to show/hide the element.
* *
@ -140,7 +161,7 @@ class DisplayRule {
public function action_visible( string $selector ): self { public function action_visible( string $selector ): self {
$this->add_action( $this->add_action(
array( array(
'type' => self::ACTION_TYPE_ELEMENT, 'type' => self::ACTION_TYPE_VISIBILITY,
'selector' => $selector, 'selector' => $selector,
'action' => self::ACTION_VISIBLE, 'action' => self::ACTION_VISIBLE,
) )
@ -148,6 +169,24 @@ class DisplayRule {
return $this; 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. * Adds a condition to enable/disable the element.
* *
@ -156,7 +195,7 @@ class DisplayRule {
public function action_enable( string $selector ): self { public function action_enable( string $selector ): self {
$this->add_action( $this->add_action(
array( array(
'type' => self::ACTION_TYPE_ELEMENT, 'type' => self::ACTION_TYPE_VISIBILITY,
'selector' => $selector, 'selector' => $selector,
'action' => self::ACTION_ENABLE, 'action' => self::ACTION_ENABLE,
) )

View file

@ -140,7 +140,13 @@ trait CreditCardOrderInfoHandlingTrait {
); );
$wc_order->add_order_note( $cvv_response_order_note ); $wc_order->add_order_note( $cvv_response_order_note );
$meta_details = array_merge( $fraud_responses, array( 'card_brand' => $card_brand ) ); $meta_details = array_merge(
$fraud_responses,
array(
'card_brand' => $card_brand,
'card_last_digits' => $card_last_digits,
)
);
$wc_order->update_meta_data( PayPalGateway::FRAUD_RESULT_META_KEY, $meta_details ); $wc_order->update_meta_data( PayPalGateway::FRAUD_RESULT_META_KEY, $meta_details );
$wc_order->save_meta_data(); $wc_order->save_meta_data();

View file

@ -270,6 +270,40 @@ class OrderProcessor {
do_action( 'woocommerce_paypal_payments_after_order_processor', $wc_order, $order ); 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. * 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, 'desc_tip' => true,
'label' => $container->get( 'wcgateway.settings.fraudnet-label' ), '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' ), '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( 'screens' => array(
State::STATE_ONBOARDED, State::STATE_ONBOARDED,
), ),
@ -522,8 +522,8 @@ return function ( ContainerInterface $container, array $fields ): array {
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'options' => array( '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_EXTRA_LINE => __( 'Add another line item', 'woocommerce-paypal-payments' ),
PurchaseUnitSanitizer::MODE_DITCH => __( 'Do not send line items to PayPal', 'woocommerce-paypal-payments' ),
), ),
'screens' => array( 'screens' => array(
State::STATE_START, State::STATE_START,
@ -538,6 +538,7 @@ return function ( ContainerInterface $container, array $fields ): array {
->rule() ->rule()
->condition_element( 'subtotal_mismatch_behavior', PurchaseUnitSanitizer::MODE_EXTRA_LINE ) ->condition_element( 'subtotal_mismatch_behavior', PurchaseUnitSanitizer::MODE_EXTRA_LINE )
->action_visible( 'subtotal_mismatch_line_name' ) ->action_visible( 'subtotal_mismatch_line_name' )
->action_class( 'subtotal_mismatch_behavior', 'active' )
->to_array(), ->to_array(),
) )
), ),
@ -548,6 +549,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'type' => 'text', 'type' => 'text',
'desc_tip' => true, 'desc_tip' => true,
'description' => __( 'The name of the extra line that will be sent to PayPal to correct the subtotal mismatch.', 'woocommerce-paypal-payments' ), '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, 'maxlength' => 22,
'default' => '', 'default' => '',
'screens' => array( 'screens' => array(

View file

@ -91,7 +91,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'title' => __( 'Customize Smart Buttons Per Location', 'woocommerce-paypal-payments' ), 'title' => __( 'Customize Smart Buttons Per Location', 'woocommerce-paypal-payments' ),
'type' => 'checkbox', 'type' => 'checkbox',
'label' => __( 'Customize smart button style per location', 'woocommerce-paypal-payments' ), 'label' => __( 'Customize smart button style per location', 'woocommerce-paypal-payments' ),
'default' => true, 'default' => false,
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ), 'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(), 'requirements' => array(),
'gateway' => 'paypal', 'gateway' => 'paypal',

View file

@ -142,7 +142,7 @@ class Settings implements ContainerInterface {
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'smart_button_locations' => $this->default_button_locations, '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_messaging_enabled' => true,
'pay_later_button_enabled' => true, 'pay_later_button_enabled' => true,
'pay_later_button_locations' => $this->default_pay_later_button_locations, 'pay_later_button_locations' => $this->default_pay_later_button_locations,

View file

@ -262,7 +262,7 @@ class SettingsRenderer {
$html = sprintf( $html = sprintf(
'<h3 class="wc-settings-sub-title %s">%s</h3>', '<h3 class="wc-settings-sub-title %s">%s</h3>',
esc_attr( implode( ' ', $config['class'] ) ), esc_attr( implode( ' ', $config['class'] ) ),
esc_html( $config['heading'] ) isset( $config['heading_html'] ) ? $config['heading_html'] : esc_html( $config['heading'] )
); );
return $html; return $html;
@ -388,7 +388,12 @@ $data_rows_html
<th scope="row"> <th scope="row">
<label <label
for="<?php echo esc_attr( $id ); ?>" for="<?php echo esc_attr( $id ); ?>"
><?php echo esc_html( $config['title'] ); ?></label> >
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo isset( $config['title_html'] ) ? $config['title_html'] : esc_html( $config['title'] );
?>
</label>
<?php if ( isset( $config['desc_tip'] ) && $config['desc_tip'] ) : ?> <?php if ( isset( $config['desc_tip'] ) && $config['desc_tip'] ) : ?>
<span <span
class="woocommerce-help-tip" class="woocommerce-help-tip"

View file

@ -315,4 +315,29 @@ class SubscriptionHelper {
return ''; return '';
} }
/**
* Returns the variation subscription plan id from the cart.
*
* @return string
*/
public function paypal_subscription_variation_from_cart(): string {
$cart = WC()->cart ?? null;
if ( ! $cart || $cart->is_empty() ) {
return '';
}
$items = $cart->get_cart_contents();
foreach ( $items as $item ) {
$variation_id = $item['variation_id'] ?? 0;
if ( $variation_id ) {
$variation_product = wc_get_product( $variation_id ) ?? '';
if ( $variation_product && $variation_product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $variation_product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
}
return '';
}
} }

View file

@ -19,6 +19,7 @@
"install:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn install", "install:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn install",
"install:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn install", "install:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn install",
"install:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn install", "install:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn install",
"install:modules:ppcp-axo": "cd modules/ppcp-axo && yarn install",
"install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install", "install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install", "install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
"install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install", "install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install",
@ -33,6 +34,7 @@
"build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build", "build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build",
"build:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run build", "build:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run build",
"build:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run build", "build:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run build",
"build:modules:ppcp-axo": "cd modules/ppcp-axo && yarn run build",
"build:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run build", "build:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run build",
"build:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run build", "build:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run build",
"build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build", "build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build",
@ -50,6 +52,7 @@
"watch:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run watch", "watch:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run watch",
"watch:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run watch", "watch:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run watch",
"watch:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run watch", "watch:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run watch",
"watch:modules:ppcp-axo": "cd modules/ppcp-axo && yarn run watch",
"watch:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run watch", "watch:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run watch",
"watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch", "watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch",
"watch:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run watch", "watch:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run watch",

View file

@ -0,0 +1,20 @@
<?php
namespace WooCommerce\PayPalCommerce\ApiClient\Exception;
use WooCommerce\PayPalCommerce\TestCase;
class PayPalApiExceptionTest extends TestCase
{
public function testFriendlyMessage()
{
$testee = new PayPalApiException();
$response = json_decode('{"details":[{"issue":"PAYMENT_DENIED"}]}');
$this->assertEquals(
'PayPal rejected the payment. Please reach out to the PayPal support for more information.',
$testee->get_customer_friendly_message($response)
);
}
}

View file

@ -6,11 +6,13 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Exception; use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
@ -44,6 +46,9 @@ class WcGatewayTest extends TestCase
private $logger; private $logger;
private $apiShopCountry; private $apiShopCountry;
private $orderEndpoint; private $orderEndpoint;
private $paymentTokensEndpoint;
private $vaultV3Enabled;
private $wcPaymentTokens;
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
@ -88,6 +93,10 @@ class WcGatewayTest extends TestCase
$this->logger->shouldReceive('info'); $this->logger->shouldReceive('info');
$this->logger->shouldReceive('error'); $this->logger->shouldReceive('error');
$this->paymentTokensEndpoint = Mockery::mock(PaymentTokensEndpoint::class);
$this->vaultV3Enabled = true;
$this->wcPaymentTokens = Mockery::mock(WooCommercePaymentTokens::class);
} }
private function createGateway() private function createGateway()
@ -111,7 +120,10 @@ class WcGatewayTest extends TestCase
function ($id) { function ($id) {
return 'checkoutnow=' . $id; return 'checkoutnow=' . $id;
}, },
'Pay via PayPal' 'Pay via PayPal',
$this->paymentTokensEndpoint,
$this->vaultV3Enabled,
$this->wcPaymentTokens
); );
} }

View file

@ -36,7 +36,7 @@ class ApplicationContextRepositoryTest extends TestCase
->andReturn($value); ->andReturn($value);
} }
expect('network_home_url') expect('home_url')
->andReturn('https://example.com/'); ->andReturn('https://example.com/');
expect('wc_get_checkout_url') expect('wc_get_checkout_url')
->andReturn('https://example.com/checkout/'); ->andReturn('https://example.com/checkout/');

View file

@ -37,6 +37,53 @@ test('Save during purchase', async ({page}) => {
await expectOrderReceivedPage(page); await expectOrderReceivedPage(page);
}); });
test('PayPal add payment method', async ({page}) => {
await loginAsCustomer(page);
await page.goto('/my-account/add-payment-method');
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
popup.locator('#consentButton').click();
await page.waitForURL('/my-account/payment-methods');
});
test('ACDC add payment method', async ({page}) => {
await loginAsCustomer(page);
await page.goto('/my-account/add-payment-method');
await page.click("text=Debit & Credit Cards");
const creditCardNumber = await page.frameLocator('[title="paypal_card_number_field"]').locator('.card-field-number');
await creditCardNumber.fill('4005519200000004');
const expirationDate = await page.frameLocator('[title="paypal_card_expiry_field"]').locator('.card-field-expiry');
await expirationDate.fill('01/25');
const cvv = await page.frameLocator('[title="paypal_card_cvv_field"]').locator('.card-field-cvv');
await cvv.fill('123');
await page.waitForURL('/my-account/payment-methods');
});
test('PayPal logged-in user free trial subscription without payment token', async ({page}) => {
await loginAsCustomer(page);
await page.goto('/shop');
await page.click("text=Sign up now");
await page.goto('/classic-checkout');
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
popup.locator('#consentButton').click();
await page.click("text=Proceed to PayPal");
const title = await page.locator('.entry-title');
await expect(title).toHaveText('Order received');
})