Merge pull request #1170 from woocommerce/PCP-1430-validate-free-trial

Validate before free trial
This commit is contained in:
Emili Castells 2023-02-13 15:27:14 +01:00 committed by GitHub
commit ec04c02b3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 364 additions and 3 deletions

View file

@ -18,6 +18,7 @@ import {hide, setVisible, setVisibleByClass} from "./modules/Helper/Hiding";
import {isChangePaymentPage} from "./modules/Helper/Subscriptions";
import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
import FormSaver from './modules/Helper/FormSaver';
import FormValidator from "./modules/Helper/FormValidator";
// TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough.
@ -36,7 +37,13 @@ const bootstrap = () => {
PayPalCommerceGateway.ajax.save_checkout_form.nonce,
);
const freeTrialHandler = new FreeTrialHandler(PayPalCommerceGateway, checkoutFormSelector, formSaver, spinner, errorHandler);
const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ?
new FormValidator(
PayPalCommerceGateway.ajax.validate_checkout.endpoint,
PayPalCommerceGateway.ajax.validate_checkout.nonce,
) : null;
const freeTrialHandler = new FreeTrialHandler(PayPalCommerceGateway, checkoutFormSelector, formSaver, formValidator, spinner, errorHandler);
jQuery('form.woocommerce-checkout input').on('keydown', e => {
if (e.key === 'Enter' && [

View file

@ -1,14 +1,24 @@
class FreeTrialHandler {
/**
* @param config
* @param formSelector
* @param {FormSaver} formSaver
* @param {FormValidator|null} formValidator
* @param {Spinner} spinner
* @param {ErrorHandler} errorHandler
*/
constructor(
config,
formSelector,
formSaver,
formValidator,
spinner,
errorHandler
) {
this.config = config;
this.formSelector = formSelector;
this.formSaver = formSaver;
this.formValidator = formValidator;
this.spinner = spinner;
this.errorHandler = errorHandler;
}
@ -24,6 +34,19 @@ class FreeTrialHandler {
}
try {
if (this.formValidator) {
try {
const errors = await this.formValidator.validate(document.querySelector(this.formSelector));
if (errors.length > 0) {
this.spinner.unblock();
this.errorHandler.messages(errors);
return;
}
} catch (error) {
console.error(error);
}
}
const res = await fetch(this.config.ajax.vault_paypal.endpoint, {
method: 'POST',
credentials: 'same-origin',

View file

@ -0,0 +1,31 @@
export default class FormValidator {
constructor(url, nonce) {
this.url = url;
this.nonce = nonce;
}
async validate(form) {
const formData = new FormData(form);
const formJsonObj = Object.fromEntries(formData.entries());
const res = await fetch(this.url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.nonce,
form: formJsonObj,
}),
});
const data = await res.json();
if (!data.success) {
if (data.data.errors) {
return data.data.errors;
}
throw Error(data.data.message);
}
return [];
}
}

View file

@ -9,8 +9,10 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -106,6 +108,7 @@ return array(
$currency,
$container->get( 'wcgateway.all-funding-sources' ),
$container->get( 'button.basic-checkout-validation-enabled' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
@ -210,6 +213,13 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.endpoint.validate-checkout' => static function ( ContainerInterface $container ): ValidateCheckoutEndpoint {
return new ValidateCheckoutEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'button.validation.wc-checkout-validator' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger );
@ -246,4 +256,7 @@ return array(
*/
return (bool) apply_filters( 'woocommerce_paypal_payments_early_wc_checkout_validation_enabled', true );
},
'button.validation.wc-checkout-validator' => static function ( ContainerInterface $container ): CheckoutFormValidator {
return new CheckoutFormValidator();
},
);

View file

@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
@ -156,6 +157,13 @@ class SmartButton implements SmartButtonInterface {
*/
private $basic_checkout_validation_enabled;
/**
* Whether to execute WC validation of the checkout form.
*
* @var bool
*/
protected $early_validation_enabled;
/**
* The logger.
*
@ -189,6 +197,7 @@ class SmartButton implements SmartButtonInterface {
* @param string $currency 3-letter currency code of the shop.
* @param array $all_funding_sources All existing funding sources.
* @param bool $basic_checkout_validation_enabled Whether the basic JS validation of the form iss enabled.
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -208,6 +217,7 @@ class SmartButton implements SmartButtonInterface {
string $currency,
array $all_funding_sources,
bool $basic_checkout_validation_enabled,
bool $early_validation_enabled,
LoggerInterface $logger
) {
@ -227,6 +237,7 @@ class SmartButton implements SmartButtonInterface {
$this->currency = $currency;
$this->all_funding_sources = $all_funding_sources;
$this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled;
$this->early_validation_enabled = $early_validation_enabled;
$this->logger = $logger;
}
@ -783,6 +794,10 @@ class SmartButton implements SmartButtonInterface {
'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ),
),
'validate_checkout' => array(
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
),
),
'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(),
@ -872,6 +887,7 @@ class SmartButton implements SmartButtonInterface {
'single_product_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'product' ),
'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ),
'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled,
'early_checkout_validation_enabled' => $this->early_validation_enabled,
);
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
@ -96,7 +97,7 @@ class ButtonModule implements ModuleInterface {
*
* @param ContainerInterface $container The Container.
*/
private function register_ajax_endpoints( ContainerInterface $container ) {
private function register_ajax_endpoints( ContainerInterface $container ): void {
add_action(
'wc_ajax_' . DataClientIdEndpoint::ENDPOINT,
static function () use ( $container ) {
@ -167,6 +168,15 @@ class ButtonModule implements ModuleInterface {
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . ValidateCheckoutEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.validate-checkout' );
assert( $endpoint instanceof ValidateCheckoutEndpoint );
$endpoint->handle_request();
}
);
}
/**

View file

@ -0,0 +1,107 @@
<?php
/**
* The endpoint for validating the checkout form.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
/**
* Class ValidateCheckoutEndpoint.
*/
class ValidateCheckoutEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-validate-checkout';
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The CheckoutFormValidator.
*
* @var CheckoutFormValidator
*/
private $checkout_form_validator;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* ValidateCheckoutEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param CheckoutFormValidator $checkout_form_validator The CheckoutFormValidator.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
CheckoutFormValidator $checkout_form_validator,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->checkout_form_validator = $checkout_form_validator;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$form_fields = $data['form'];
$this->checkout_form_validator->validate( $form_fields );
wp_send_json_success();
return true;
} catch ( ValidationException $exception ) {
wp_send_json_error(
array(
'message' => $exception->getMessage(),
'errors' => $exception->errors(),
)
);
return false;
} catch ( Throwable $error ) {
$this->logger->error( "Form validation execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
wp_send_json_error(
array(
'message' => $error->getMessage(),
)
);
return false;
}
}
}

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Mockery;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\expect;
class ValidateCheckoutEndpointTest extends TestCase
{
private $requestData;
private $formValidator;
private $logger;
private $sut;
public function setUp(): void
{
parent::setUp();
$this->requestData = Mockery::mock(RequestData::class);
$this->formValidator = Mockery::mock(CheckoutFormValidator::class);
$this->logger = Mockery::mock(LoggerInterface::class);
$this->sut = new ValidateCheckoutEndpoint(
$this->requestData,
$this->formValidator,
$this->logger
);
$this->requestData->expects('read_request')->andReturn(['form' => ['field1' => 'value']]);
}
public function testValid()
{
$this->formValidator->expects('validate')->once();
expect('wp_send_json_success')->once();
$this->sut->handle_request();
}
public function testInvalid()
{
$exception = new ValidationException(['Invalid value']);
$this->formValidator->expects('validate')->once()
->andThrow($exception);
expect('wp_send_json_error')->once()
->with(['message' => $exception->getMessage(), 'errors' => ['Invalid value']]);
$this->sut->handle_request();
}
public function testFailure()
{
$exception = new Exception('BOOM');
$this->formValidator->expects('validate')->once()
->andThrow($exception);
expect('wp_send_json_error')->once()
->with(['message' => $exception->getMessage()]);
$this->logger->expects('error')->once();
$this->sut->handle_request();
}
}

View file

@ -8,5 +8,6 @@ require_once ROOT_DIR . '/vendor/autoload.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway_CC.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Ajax.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Checkout.php';
Hamcrest\Util::registerGlobalFunctions();

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Tests\E2e\Validation;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Tests\E2e\TestCase;
class ValidationTest extends TestCase
{
protected $container;
/**
* @var CheckoutFormValidator
*/
protected $sut;
public function setUp(): void
{
parent::setUp();
$this->container = $this->getContainer();
$this->sut = $this->container->get( 'button.validation.wc-checkout-validator' );
assert($this->sut instanceof CheckoutFormValidator);
}
public function testValid()
{
$this->sut->validate([
'billing_first_name'=>'John',
'billing_last_name'=>'Doe',
'billing_company'=>'',
'billing_country'=>'DE',
'billing_address_1'=>'1 Main St',
'billing_address_2'=>'city1',
'billing_postcode'=>'11111',
'billing_city'=>'city1',
'billing_state'=>'DE-BW',
'billing_phone'=>'12345678',
'billing_email'=>'a@gmail.com',
'terms-field'=>'1',
'terms'=>'on',
]);
}
public function testInvalid()
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessageMatches('/.+First name.+Postcode/i');
$this->sut->validate([
'billing_first_name'=>'',
'billing_postcode'=>'ABCDE',
'billing_last_name'=>'Doe',
'billing_company'=>'',
'billing_country'=>'DE',
'billing_address_1'=>'1 Main St',
'billing_address_2'=>'city1',
'billing_city'=>'city1',
'billing_state'=>'DE-BW',
'billing_phone'=>'12345678',
'billing_email'=>'a@gmail.com',
'terms-field'=>'1',
'terms'=>'on',
]);
}
}

View file

@ -0,0 +1,11 @@
<?php
declare( strict_types=1 );
class WC_Checkout {
public function get_posted_data() {
return [];
}
protected function validate_checkout( &$data, &$errors ) {
}
}