Merge pull request #2152 from woocommerce/PCP-2768-zero-sum-subscriptions-cause-cannot-be-zero-or-negative-when-using-vault-v-3

Zero sum subscriptions cause CANNOT_BE_ZERO_OR_NEGATIVE when using Vault v3 (2768)
This commit is contained in:
Emili Castells 2024-04-11 16:42:48 +02:00 committed by GitHub
commit ffe3a0b958
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 422 additions and 46 deletions

View file

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

View file

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

View file

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

View file

@ -116,6 +116,14 @@ class CheckoutBootstap {
return; return;
} }
if(
PayPalCommerceGateway.is_free_trial_cart
&& PayPalCommerceGateway.vault_v3_enabled
) {
this.renderer.render(actionHandler.addPaymentMethodConfiguration(), {}, actionHandler.configuration());
return;
}
this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration()); this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration());
} }

View file

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

View file

@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WC_Product; use WC_Product;
use WC_Product_Variation; use WC_Product_Variation;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
@ -33,6 +34,9 @@ use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule; use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
@ -184,13 +188,6 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $funding_sources_without_redirect; private $funding_sources_without_redirect;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/** /**
* Session handler. * Session handler.
* *
@ -198,6 +195,27 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $session_handler; private $session_handler;
/**
* Whether Vault v3 module is enabled.
*
* @var bool
*/
private $vault_v3_enabled;
/**
* Payment tokens endpoint.
*
* @var PaymentTokensEndpoint
*/
private $payment_tokens_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/** /**
* SmartButton constructor. * SmartButton constructor.
* *
@ -220,6 +238,8 @@ class SmartButton implements SmartButtonInterface {
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param array $pay_now_contexts The contexts that should have the Pay Now button. * @param array $pay_now_contexts The contexts that should have the Pay Now button.
* @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back. * @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back.
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
@ -242,6 +262,8 @@ class SmartButton implements SmartButtonInterface {
bool $early_validation_enabled, bool $early_validation_enabled,
array $pay_now_contexts, array $pay_now_contexts,
array $funding_sources_without_redirect, array $funding_sources_without_redirect,
bool $vault_v3_enabled,
PaymentTokensEndpoint $payment_tokens_endpoint,
LoggerInterface $logger LoggerInterface $logger
) { ) {
@ -264,7 +286,9 @@ class SmartButton implements SmartButtonInterface {
$this->early_validation_enabled = $early_validation_enabled; $this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts; $this->pay_now_contexts = $pay_now_contexts;
$this->funding_sources_without_redirect = $funding_sources_without_redirect; $this->funding_sources_without_redirect = $funding_sources_without_redirect;
$this->vault_v3_enabled = $vault_v3_enabled;
$this->logger = $logger; $this->logger = $logger;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
} }
/** /**
@ -1035,44 +1059,57 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
'redirect' => wc_get_checkout_url(), 'redirect' => wc_get_checkout_url(),
'context' => $this->context(), 'context' => $this->context(),
'ajax' => array( 'ajax' => array(
'simulate_cart' => array( 'simulate_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ), 'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ),
), ),
'change_cart' => array( 'change_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
), ),
'create_order' => array( 'create_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ),
), ),
'approve_order' => array( 'approve_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ),
), ),
'approve_subscription' => array( 'approve_subscription' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ), 'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ),
), ),
'vault_paypal' => array( 'vault_paypal' => array(
'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ),
), ),
'save_checkout_form' => array( 'save_checkout_form' => array(
'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ), 'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ),
), ),
'validate_checkout' => array( 'validate_checkout' => array(
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
), ),
'cart_script_params' => array( 'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
), ),
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),
),
'create_payment_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ),
),
'create_payment_token_for_guest' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentTokenForGuest::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentTokenForGuest::nonce() ),
),
), ),
'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(), 'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(),
'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(), 'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(),
'vault_v3_enabled' => $this->vault_v3_enabled,
'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(), 'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(),
'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(), 'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(),
'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(), 'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(),
@ -1904,8 +1941,18 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
*/ */
private function get_vaulted_paypal_email(): string { private function get_vaulted_paypal_email(): string {
try { try {
$tokens = $this->get_payment_tokens(); $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( $customer_id ) {
$customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id );
foreach ( $customer_tokens as $token ) {
$email_address = $token['payment_source']->properties()->email_address ?? '';
if ( $email_address ) {
return $email_address;
}
}
}
$tokens = $this->get_payment_tokens();
foreach ( $tokens as $token ) { foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) { if ( isset( $token->source()->paypal ) ) {
return $token->source()->paypal->payer->email_address; return $token->source()->paypal->payer->email_address;
@ -1914,6 +1961,7 @@ document.querySelector("#payment").before(document.querySelector("#ppcp-messages
} catch ( Exception $exception ) { } catch ( Exception $exception ) {
$this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() ); $this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() );
} }
return ''; return '';
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -103,7 +103,10 @@ return array(
$api_shop_country, $api_shop_country,
$container->get( 'api.endpoint.order' ), $container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.paypal-checkout-url' ), $container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.place-order-button-text' ) $container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'vaulting.wc-payment-tokens' )
); );
}, },
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {

View file

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

View file

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

View file

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

View file

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