Merge pull request #2155 from woocommerce/PCP-2347-new-feature-accelerated-checkout

New feature: Accelerated Checkout (2347)
This commit is contained in:
Emili Castells 2024-04-18 16:04:39 +02:00 committed by GitHub
commit a3bbcfeec5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 5702 additions and 58 deletions

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

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

View file

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-axo",
"type": "dhii-mod",
"description": "Axo module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\Axo\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,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

@ -60,6 +60,13 @@ export const loadPaypalScript = (config, onLoaded, onError = null) => {
scriptOptions = merge(scriptOptions, config.script_attributes);
}
// Axo SDK options
const sdkClientToken = config?.axo?.sdk_client_token;
if(sdkClientToken) {
scriptOptions['data-sdk-client-token'] = sdkClientToken;
scriptOptions['data-client-metadata-id'] = 'ppcp-cm-id';
}
// Load PayPal script for special case with data-client-token
if (config.data_client_id?.set_attribute) {
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback, errorCallback);

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

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

View file

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

View file

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

View file

@ -1,5 +1,8 @@
@use "../../../ppcp-button/resources/css/mixins/apm-button" as apm-button;
$border-color: #c3c3c3;
$background-ident-color: #fbfbfb;
.ppcp-field-hidden {
display: none !important;
}
@ -11,9 +14,10 @@
opacity: 0.5;
}
.ppcp-field-indent {
th {
padding-left: 20px;
.ppcp-active-spacer {
th, td {
padding: 0;
height: 1rem;
}
}
@ -39,3 +43,51 @@
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 {
static make(actionConfig) {
switch (actionConfig.type) {
case 'element':
return new ElementAction(actionConfig);
case 'visibility':
return new VisibilityAction(actionConfig);
case 'attribute':
return new AttributeAction(actionConfig);
}
throw new Error('[ActionFactory] Unknown action: ' + actionConfig.type);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -538,6 +538,7 @@ return function ( ContainerInterface $container, array $fields ): array {
->rule()
->condition_element( 'subtotal_mismatch_behavior', PurchaseUnitSanitizer::MODE_EXTRA_LINE )
->action_visible( 'subtotal_mismatch_line_name' )
->action_class( 'subtotal_mismatch_behavior', 'active' )
->to_array(),
)
),
@ -548,6 +549,7 @@ return function ( ContainerInterface $container, array $fields ): array {
'type' => 'text',
'desc_tip' => true,
'description' => __( 'The name of the extra line that will be sent to PayPal to correct the subtotal mismatch.', 'woocommerce-paypal-payments' ),
'classes' => array( 'ppcp-field-indent' ),
'maxlength' => 22,
'default' => '',
'screens' => array(

View file

@ -262,7 +262,7 @@ class SettingsRenderer {
$html = sprintf(
'<h3 class="wc-settings-sub-title %s">%s</h3>',
esc_attr( implode( ' ', $config['class'] ) ),
esc_html( $config['heading'] )
isset( $config['heading_html'] ) ? $config['heading_html'] : esc_html( $config['heading'] )
);
return $html;
@ -388,7 +388,12 @@ $data_rows_html
<th scope="row">
<label
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'] ) : ?>
<span
class="woocommerce-help-tip"