diff --git a/changelog.txt b/changelog.txt index 6879bff12..1dea43cea 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,14 +1,14 @@ *** Changelog *** -= 1.9.1 - TBD = += 1.9.1 - 2022-07-25 = * Fix - ITEM_TOTAL_MISMATCH error when checking out with multiple products #721 * Fix - Unable to purchase a product with Credit card button in pay for order page #718 * Fix - Pay Later messaging only displayed when smart button is active on the same page #283 * Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667 * Fix - Placeholders and card type detection not working for PayPal Card Processing (260) #685 -* Fix - PUI gateway is displayed with unsupported store currency #711 +* Fix - PUI gateway is displayed with unsupported store currency #711 * Fix - Wrong PUI locale sent causing error PAYMENT_SOURCE_CANNOT_BE_USED #741 -* Enhancement - Missing PayPal fee in WC order details for PUI purchase #714 +* Enhancement - Missing PayPal fee in WC order details for PUI purchase #714 * Enhancement - Skip loading of PUI js file on all pages where PUI gateway is not displayed #723 * Enhancement - PUI feature capitalization not consistent #724 diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index f34ac93da..ef1b40e3d 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -400,6 +400,60 @@ return array( ); }, + + 'api.shop.is-latin-america' => static function ( ContainerInterface $container ): bool { + return in_array( + $container->get( 'api.shop.country' ), + array( + 'AI', + 'AG', + 'AR', + 'AW', + 'BS', + 'BB', + 'BZ', + 'BM', + 'BO', + 'BR', + 'VG', + 'KY', + 'CL', + 'CO', + 'CR', + 'DM', + 'DO', + 'EC', + 'SV', + 'FK', + 'GF', + 'GD', + 'GP', + 'GT', + 'GY', + 'HN', + 'JM', + 'MQ', + 'MX', + 'MS', + 'AN', + 'NI', + 'PA', + 'PY', + 'PE', + 'KN', + 'LC', + 'PM', + 'VC', + 'SR', + 'TT', + 'TC', + 'UY', + 'VE', + ), + true + ); + }, + /** * Currencies supported by PayPal. * diff --git a/modules/ppcp-api-client/src/Endpoint/IdentityToken.php b/modules/ppcp-api-client/src/Endpoint/IdentityToken.php index a01e746ac..0c27e3fe4 100644 --- a/modules/ppcp-api-client/src/Endpoint/IdentityToken.php +++ b/modules/ppcp-api-client/src/Endpoint/IdentityToken.php @@ -106,7 +106,7 @@ class IdentityToken { && defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION ) { $customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) ); - + update_user_meta( $user_id, 'ppcp_customer_id', $customer_id ); $args['body'] = wp_json_encode( array( 'customer_id' => $customer_id, diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index abefeb38a..57b8b73d6 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -194,7 +194,7 @@ class OrderEndpoint { 'application_context' => $this->application_context_repository ->current_context( $shipping_preference )->to_array(), ); - if ( $payer && ! empty( $payer->email_address() ) && ! empty( $payer->name() ) ) { + if ( $payer && ! empty( $payer->email_address() ) ) { $data['payer'] = $payer->to_array(); } if ( $payment_token ) { diff --git a/modules/ppcp-api-client/src/Entity/Payer.php b/modules/ppcp-api-client/src/Entity/Payer.php index d20fd215a..45164c1f5 100644 --- a/modules/ppcp-api-client/src/Entity/Payer.php +++ b/modules/ppcp-api-client/src/Entity/Payer.php @@ -18,7 +18,7 @@ class Payer { /** * The name. * - * @var PayerName + * @var PayerName|null */ private $name; @@ -46,7 +46,7 @@ class Payer { /** * The address. * - * @var Address + * @var Address|null */ private $address; @@ -67,7 +67,7 @@ class Payer { /** * Payer constructor. * - * @param PayerName $name The name. + * @param PayerName|null $name The name. * @param string $email_address The email. * @param string $payer_id The payer id. * @param Address|null $address The address. @@ -76,7 +76,7 @@ class Payer { * @param PayerTaxInfo|null $tax_info The tax info. */ public function __construct( - PayerName $name, + ?PayerName $name, string $email_address, string $payer_id, Address $address = null, @@ -97,12 +97,21 @@ class Payer { /** * Returns the name. * - * @return PayerName + * @return PayerName|null */ - public function name(): PayerName { + public function name(): ?PayerName { return $this->name; } + /** + * Sets the name. + * + * @param PayerName|null $name The value. + */ + public function set_name( ?PayerName $name ): void { + $this->name = $name; + } + /** * Returns the email address. * @@ -139,6 +148,15 @@ class Payer { return $this->address; } + /** + * Sets the address. + * + * @param Address|null $address The value. + */ + public function set_address( ?Address $address ): void { + $this->address = $address; + } + /** * Returns the phone. * @@ -164,27 +182,26 @@ class Payer { */ public function to_array() { $payer = array( - 'name' => $this->name()->to_array(), 'email_address' => $this->email_address(), ); - if ( $this->address() ) { - $payer['address'] = $this->address->to_array(); - if ( 2 !== strlen( $this->address()->country_code() ) ) { - unset( $payer['address'] ); - } + if ( $this->name ) { + $payer['name'] = $this->name->to_array(); } - if ( $this->payer_id() ) { - $payer['payer_id'] = $this->payer_id(); + if ( $this->address && 2 === strlen( $this->address->country_code() ) ) { + $payer['address'] = $this->address->to_array(); + } + if ( $this->payer_id ) { + $payer['payer_id'] = $this->payer_id; } - if ( $this->phone() ) { - $payer['phone'] = $this->phone()->to_array(); + if ( $this->phone ) { + $payer['phone'] = $this->phone->to_array(); } - if ( $this->tax_info() ) { - $payer['tax_info'] = $this->tax_info()->to_array(); + if ( $this->tax_info ) { + $payer['tax_info'] = $this->tax_info->to_array(); } - if ( $this->birthdate() ) { - $payer['birth_date'] = $this->birthdate()->format( 'Y-m-d' ); + if ( $this->birthdate ) { + $payer['birth_date'] = $this->birthdate->format( 'Y-m-d' ); } return $payer; } diff --git a/modules/ppcp-api-client/src/Exception/PayPalApiException.php b/modules/ppcp-api-client/src/Exception/PayPalApiException.php index d5720532b..d2752cb72 100644 --- a/modules/ppcp-api-client/src/Exception/PayPalApiException.php +++ b/modules/ppcp-api-client/src/Exception/PayPalApiException.php @@ -111,15 +111,6 @@ class PayPalApiException extends RuntimeException { return false; } - /** - * Returns response issues. - * - * @return array - */ - public function issues(): array { - return $this->response->issues ?? array(); - } - /** * The HTTP status code. * diff --git a/modules/ppcp-api-client/src/Factory/AmountFactory.php b/modules/ppcp-api-client/src/Factory/AmountFactory.php index 71f1c1a9d..51a6d8c11 100644 --- a/modules/ppcp-api-client/src/Factory/AmountFactory.php +++ b/modules/ppcp-api-client/src/Factory/AmountFactory.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -132,7 +133,7 @@ class AmountFactory { $total_value = (float) $order->get_total(); if ( ( - CreditCardGateway::ID === $order->get_payment_method() + in_array( $order->get_payment_method(), array( CreditCardGateway::ID, CardButtonGateway::ID ), true ) || ( PayPalGateway::ID === $order->get_payment_method() && 'card' === $order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) ) ) && $this->is_free_trial_order( $order ) diff --git a/modules/ppcp-api-client/src/Repository/CustomerRepository.php b/modules/ppcp-api-client/src/Repository/CustomerRepository.php index 99063b7e3..c4f172dc8 100644 --- a/modules/ppcp-api-client/src/Repository/CustomerRepository.php +++ b/modules/ppcp-api-client/src/Repository/CustomerRepository.php @@ -57,6 +57,6 @@ class CustomerRepository { return $guest_customer_id; } - return $this->prefix . (string) $user_id; + return get_user_meta( $user_id, 'ppcp_customer_id', true ) ?: $this->prefix . (string) $user_id; } } diff --git a/modules/ppcp-button/package.json b/modules/ppcp-button/package.json index ada30892e..03403b794 100644 --- a/modules/ppcp-button/package.json +++ b/modules/ppcp-button/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "GPL-3.0-or-later", "main": "resources/js/button.js", + "dependencies": { + "deepmerge": "^4.2.2" + }, "devDependencies": { "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.5", diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index ed8c400cf..4ec22c7d7 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -18,7 +18,9 @@ import {hide, setVisible} from "./modules/Helper/Hiding"; import {isChangePaymentPage} from "./modules/Helper/Subscriptions"; import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler"; -const buttonsSpinner = new Spinner('.ppc-button-wrapper'); +// 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. +const buttonsSpinner = new Spinner(document.querySelector('.ppc-button-wrapper')); const cardsSpinner = new Spinner('#ppcp-hosted-fields'); const bootstrap = () => { @@ -38,9 +40,36 @@ const bootstrap = () => { requiredFields.each((i, input) => { jQuery(input).trigger('validate'); }); - if (jQuery('form.woocommerce-checkout .validate-required.woocommerce-invalid:visible').length) { + const invalidFields = Array.from(jQuery('form.woocommerce-checkout .validate-required.woocommerce-invalid:visible')); + if (invalidFields.length) { + const billingFieldsContainer = document.querySelector('.woocommerce-billing-fields'); + const shippingFieldsContainer = document.querySelector('.woocommerce-shipping-fields'); + + const nameMessageMap = PayPalCommerceGateway.labels.error.required.elements; + const messages = invalidFields.map(el => { + const name = el.querySelector('[name]')?.getAttribute('name'); + if (name && name in nameMessageMap) { + return nameMessageMap[name]; + } + let label = el.querySelector('label').textContent + .replaceAll('*', '') + .trim(); + if (billingFieldsContainer?.contains(el)) { + label = PayPalCommerceGateway.labels.billing_field.replace('%s', label); + } + if (shippingFieldsContainer?.contains(el)) { + label = PayPalCommerceGateway.labels.shipping_field.replace('%s', label); + } + return PayPalCommerceGateway.labels.error.required.field + .replace('%s', `${label}`) + }).filter(s => s.length > 2); + errorHandler.clear(); - errorHandler.message(PayPalCommerceGateway.labels.error.js_validation); + if (messages.length) { + messages.forEach(s => errorHandler.message(s)); + } else { + errorHandler.message(PayPalCommerceGateway.labels.error.required.generic); + } return actions.reject(); } @@ -83,7 +112,7 @@ const bootstrap = () => { PayPalCommerceGateway, renderer, messageRenderer, - ); + );w singleProductBootstrap.init(); } @@ -138,6 +167,11 @@ document.addEventListener( return; } + const paypalButtonGatewayIds = [ + PaymentMethods.PAYPAL, + ...Object.entries(PayPalCommerceGateway.separate_buttons).map(([k, data]) => data.id), + ] + // Sometimes PayPal script takes long time to load, // so we additionally hide the standard order button here to avoid failed orders. // Normally it is hidden later after the script load. @@ -153,12 +187,12 @@ document.addEventListener( } const currentPaymentMethod = getCurrentPaymentMethod(); - const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL; + const isPaypalButton = paypalButtonGatewayIds.includes(currentPaymentMethod); const isCards = currentPaymentMethod === PaymentMethods.CARDS; - setVisible(ORDER_BUTTON_SELECTOR, !isPaypal && !isCards, true); + setVisible(ORDER_BUTTON_SELECTOR, !isPaypalButton && !isCards, true); - if (isPaypal) { + if (isPaypalButton) { // stopped after the first rendering of the buttons, in onInit buttonsSpinner.block(); } else { diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index 733b5e339..8b4dd7501 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -26,6 +26,9 @@ class CheckoutActionHandler { const createaccount = jQuery('#createaccount').is(":checked") ? true : false; + const paymentMethod = getCurrentPaymentMethod(); + const fundingSource = window.ppcpFundingSource; + return fetch(this.config.ajax.create_order.endpoint, { method: 'POST', body: JSON.stringify({ @@ -34,8 +37,8 @@ class CheckoutActionHandler { bn_code:bnCode, context:this.config.context, order_id:this.config.order_id, - payment_method: getCurrentPaymentMethod(), - funding_source: window.ppcpFundingSource, + payment_method: paymentMethod, + funding_source: fundingSource, form: formJsonObj, createaccount: createaccount }) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js index 29e6a4819..d32ca440f 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js @@ -32,9 +32,7 @@ class CartBootstrap { ); this.renderer.render( - this.gateway.button.wrapper, - this.gateway.hosted_fields.wrapper, - actionHandler.configuration(), + actionHandler.configuration() ); } } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index bc83c736e..9d008d4b6 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -69,9 +69,7 @@ class CheckoutBootstap { ); this.renderer.render( - this.gateway.button.wrapper, - this.gateway.hosted_fields.wrapper, - actionHandler.configuration(), + actionHandler.configuration() ); this.buttonChangeObserver.observe( @@ -84,16 +82,27 @@ class CheckoutBootstap { const currentPaymentMethod = getCurrentPaymentMethod(); const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL; const isCard = currentPaymentMethod === PaymentMethods.CARDS; + const isSeparateButtonGateway = [PaymentMethods.CARD_BUTTON].includes(currentPaymentMethod); const isSavedCard = isCard && isSavedCardSelected(); - const isNotOurGateway = !isPaypal && !isCard; + const isNotOurGateway = !isPaypal && !isCard && !isSeparateButtonGateway; const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const hasVaultedPaypal = PayPalCommerceGateway.vaulted_paypal_email !== ''; + const paypalButtonWrappers = { + ...Object.entries(PayPalCommerceGateway.separate_buttons) + .reduce((result, [k, data]) => { + return {...result, [data.id]: data.wrapper} + }, {}), + }; + setVisible(this.standardOrderButtonSelector, (isPaypal && isFreeTrial && hasVaultedPaypal) || isNotOurGateway || isSavedCard, true); setVisible('.ppcp-vaulted-paypal-details', isPaypal); setVisible(this.gateway.button.wrapper, isPaypal && !(isFreeTrial && hasVaultedPaypal)); setVisible(this.gateway.messages.wrapper, isPaypal && !isFreeTrial); setVisible(this.gateway.hosted_fields.wrapper, isCard && !isSavedCard); + for (const [gatewayId, wrapper] of Object.entries(paypalButtonWrappers)) { + setVisible(wrapper, gatewayId === currentPaymentMethod); + } if (isPaypal && !isFreeTrial) { this.messages.render(); diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js index 1a45e6814..35465c12e 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js @@ -32,9 +32,13 @@ class MiniCartBootstap { } this.renderer.render( - this.gateway.button.mini_cart_wrapper, - this.gateway.hosted_fields.mini_cart_wrapper, - this.actionHandler.configuration() + this.actionHandler.configuration(), + { + button: { + wrapper: this.gateway.button.mini_cart_wrapper, + style: this.gateway.button.mini_cart_style, + }, + } ); } } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index 6da17f4e9..ecb44eabe 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -85,9 +85,7 @@ class SingleProductBootstap { ); this.renderer.render( - this.gateway.button.wrapper, - this.gateway.hosted_fields.wrapper, - actionHandler.configuration(), + actionHandler.configuration() ); } } diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js index c98ef779a..7d5292251 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js +++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js @@ -2,6 +2,7 @@ export const PaymentMethods = { PAYPAL: 'ppcp-gateway', CARDS: 'ppcp-credit-card-gateway', OXXO: 'ppcp-oxxo-gateway', + CARD_BUTTON: 'ppcp-card-button-gateway', }; export const ORDER_BUTTON_SELECTOR = '#place_order'; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index cc811bb58..1bd4ef757 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -1,33 +1,86 @@ +import merge from "deepmerge"; + class Renderer { - constructor(creditCardRenderer, defaultConfig, onSmartButtonClick, onSmartButtonsInit) { - this.defaultConfig = defaultConfig; + constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) { + this.defaultSettings = defaultSettings; this.creditCardRenderer = creditCardRenderer; this.onSmartButtonClick = onSmartButtonClick; this.onSmartButtonsInit = onSmartButtonsInit; + + this.renderedSources = new Set(); } - render(wrapper, hostedFieldsWrapper, contextConfig) { + render(contextConfig, settingsOverride = {}) { + const settings = merge(this.defaultSettings, settingsOverride); - this.renderButtons(wrapper, contextConfig); - this.creditCardRenderer.render(hostedFieldsWrapper, contextConfig); + const enabledSeparateGateways = Object.fromEntries(Object.entries( + settings.separate_buttons).filter(([s, data]) => document.querySelector(data.wrapper) + )); + const hasEnabledSeparateGateways = Object.keys(enabledSeparateGateways).length !== 0; + + if (!hasEnabledSeparateGateways) { + this.renderButtons( + settings.button.wrapper, + settings.button.style, + contextConfig + ); + } else { + // render each button separately + for (const fundingSource of paypal.getFundingSources().filter(s => !(s in enabledSeparateGateways))) { + let style = settings.button.style; + if (fundingSource !== 'paypal') { + style = { + shape: style.shape, + }; + } + + this.renderButtons( + settings.button.wrapper, + style, + contextConfig, + fundingSource + ); + } + } + + this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfig); + + for (const [fundingSource, data] of Object.entries(enabledSeparateGateways)) { + this.renderButtons( + data.wrapper, + data.style, + contextConfig, + fundingSource + ); + } } - renderButtons(wrapper, contextConfig) { - if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper) || 'undefined' === typeof paypal.Buttons ) { + renderButtons(wrapper, style, contextConfig, fundingSource = null) { + if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource) || 'undefined' === typeof paypal.Buttons ) { return; } - const style = wrapper === this.defaultConfig.button.wrapper ? this.defaultConfig.button.style : this.defaultConfig.button.mini_cart_style; - paypal.Buttons({ + if (fundingSource) { + contextConfig.fundingSource = fundingSource; + } + + const btn = paypal.Buttons({ style, ...contextConfig, onClick: this.onSmartButtonClick, onInit: this.onSmartButtonsInit, - }).render(wrapper); + }); + if (!btn.isEligible()) { + return; + } + + btn.render(wrapper); + + this.renderedSources.add(wrapper + fundingSource ?? ''); } - isAlreadyRendered(wrapper) { - return document.querySelector(wrapper).hasChildNodes(); + isAlreadyRendered(wrapper, fundingSource) { + return this.renderedSources.has(wrapper + fundingSource ?? ''); } hideButtons(element) { diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 3b4b710fd..cd30058cf 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -133,6 +133,7 @@ return array( $settings, $early_order_handler, $registration_needed, + $container->get( 'wcgateway.settings.card_billing_data_mode' ), $logger ); }, diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 33a2eb1a4..ab6a30ffd 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -28,7 +28,9 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -421,15 +423,20 @@ class SmartButton implements SmartButtonInterface { ) { add_action( $this->single_product_renderer_hook(), - array( - $this, - 'button_renderer', - ), + function () { + $this->button_renderer( PayPalGateway::ID ); + }, 31 ); } - add_action( $this->pay_order_renderer_hook(), array( $this, 'button_renderer' ), 10 ); + add_action( + $this->pay_order_renderer_hook(), + function (): void { + $this->button_renderer( PayPalGateway::ID ); + $this->button_renderer( CardButtonGateway::ID ); + } + ); $not_enabled_on_minicart = $this->settings->has( 'button_mini_cart_enabled' ) && ! $this->settings->get( 'button_mini_cart_enabled' ); @@ -457,7 +464,13 @@ class SmartButton implements SmartButtonInterface { ); } - add_action( $this->checkout_button_renderer_hook(), array( $this, 'button_renderer' ), 10 ); + add_action( + $this->checkout_button_renderer_hook(), + function (): void { + $this->button_renderer( PayPalGateway::ID ); + $this->button_renderer( CardButtonGateway::ID ); + } + ); $not_enabled_on_cart = $this->settings->has( 'button_cart_enabled' ) && ! $this->settings->get( 'button_cart_enabled' ); @@ -468,7 +481,7 @@ class SmartButton implements SmartButtonInterface { return; } - $this->button_renderer(); + $this->button_renderer( PayPalGateway::ID ); }, 20 ); @@ -524,8 +537,10 @@ class SmartButton implements SmartButtonInterface { /** * Renders the HTML for the buttons. + * + * @param string $gateway_id The gateway ID, like 'ppcp-gateway'. */ - public function button_renderer() { + public function button_renderer( string $gateway_id ) { if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) { return; @@ -543,13 +558,13 @@ class SmartButton implements SmartButtonInterface { $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - if ( ! isset( $available_gateways['ppcp-gateway'] ) ) { + if ( ! isset( $available_gateways[ $gateway_id ] ) ) { return; } // The wrapper is needed for the loading spinner, // otherwise jQuery block() prevents buttons rendering. - echo '
'; + echo '
'; } /** @@ -810,7 +825,7 @@ class SmartButton implements SmartButtonInterface { 'bn_codes' => $this->bn_codes(), 'payer' => $this->payerData(), 'button' => array( - 'wrapper' => '#ppc-button', + 'wrapper' => '#ppc-button-' . PayPalGateway::ID, 'mini_cart_wrapper' => '#ppc-button-minicart', 'cancel_wrapper' => '#ppcp-cancel', 'url' => $this->url(), @@ -830,10 +845,19 @@ class SmartButton implements SmartButtonInterface { 'tagline' => $this->style_for_context( 'tagline', $this->context() ), ), ), + 'separate_buttons' => array( + 'card' => array( + 'id' => CardButtonGateway::ID, + 'wrapper' => '#ppc-button-' . CardButtonGateway::ID, + 'style' => array( + 'shape' => $this->style_for_context( 'shape', $this->context() ), + // TODO: color black, white from the gateway settings. + ), + ), + ), 'hosted_fields' => array( - 'wrapper' => '#ppcp-hosted-fields', - 'mini_cart_wrapper' => '#ppcp-hosted-fields-mini-cart', - 'labels' => array( + 'wrapper' => '#ppcp-hosted-fields', + 'labels' => array( 'credit_card_number' => '', 'cvv' => '', 'mm_yy' => __( 'MM/YY', 'woocommerce-paypal-payments' ), @@ -847,21 +871,36 @@ class SmartButton implements SmartButtonInterface { ), 'cardholder_name_required' => __( 'Cardholder\'s first and last name are required, please fill the checkout form required fields.', 'woocommerce-paypal-payments' ), ), - 'valid_cards' => $this->dcc_applies->valid_cards(), - 'contingency' => $this->get_3ds_contingency(), + 'valid_cards' => $this->dcc_applies->valid_cards(), + 'contingency' => $this->get_3ds_contingency(), ), 'messages' => $this->message_values(), 'labels' => array( - 'error' => array( - 'generic' => __( + 'error' => array( + 'generic' => __( 'Something went wrong. Please try again or choose another payment source.', 'woocommerce-paypal-payments' ), - 'js_validation' => __( - 'Required form fields are not filled or invalid.', - 'woocommerce-paypal-payments' + 'required' => array( + 'generic' => __( + 'Required form fields are not filled.', + 'woocommerce-paypal-payments' + ), + // phpcs:ignore WordPress.WP.I18n + 'field' => __( '%s is a required field.', 'woocommerce' ), + 'elements' => array( // Map
=> text for error messages. + 'terms' => __( + 'Please read and accept the terms and conditions to proceed with your order.', + // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch + 'woocommerce' + ), + ), ), ), + // phpcs:ignore WordPress.WP.I18n + 'billing_field' => _x( 'Billing %s', 'checkout-validation', 'woocommerce' ), + // phpcs:ignore WordPress.WP.I18n + 'shipping_field' => _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ), ), 'order_id' => 'pay-now' === $this->context() ? absint( $wp->query_vars['order-pay'] ) : 0, 'single_product_buttons_enabled' => $this->settings->has( 'button_product_enabled' ) && $this->settings->get( 'button_product_enabled' ), @@ -933,7 +972,10 @@ class SmartButton implements SmartButtonInterface { $is_dcc_enabled = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ); - if ( is_checkout() && $is_dcc_enabled ) { + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $is_separate_card_enabled = isset( $available_gateways[ CardButtonGateway::ID ] ); + + if ( is_checkout() && ( $is_dcc_enabled || $is_separate_card_enabled ) ) { $key = array_search( 'card', $disable_funding, true ); if ( false !== $key ) { unset( $disable_funding[ $key ] ); @@ -1018,6 +1060,7 @@ class SmartButton implements SmartButtonInterface { if ( $this->load_button_component() ) { $components[] = 'buttons'; + $components[] = 'funding-eligibility'; } if ( $this->messages_apply->for_country() @@ -1112,6 +1155,9 @@ class SmartButton implements SmartButtonInterface { if ( $source && $source->card() ) { return false; // Ignore for DCC. } + if ( 'card' === $this->session_handler->funding_source() ) { + return false; // Ignore for card buttons. + } return true; } diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index c0045c8c4..b9a59c490 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use stdClass; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount; +use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; @@ -27,7 +28,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; +use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -118,6 +121,13 @@ class CreateOrderEndpoint implements EndpointInterface { */ private $registration_needed; + /** + * The value of card_billing_data_mode from the settings. + * + * @var string + */ + protected $card_billing_data_mode; + /** * The logger. * @@ -137,6 +147,7 @@ class CreateOrderEndpoint implements EndpointInterface { * @param Settings $settings The Settings object. * @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object. * @param bool $registration_needed Whether a new user must be registered during checkout. + * @param string $card_billing_data_mode The value of card_billing_data_mode from the settings. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -149,6 +160,7 @@ class CreateOrderEndpoint implements EndpointInterface { Settings $settings, EarlyOrderHandler $early_order_handler, bool $registration_needed, + string $card_billing_data_mode, LoggerInterface $logger ) { @@ -161,6 +173,7 @@ class CreateOrderEndpoint implements EndpointInterface { $this->settings = $settings; $this->early_order_handler = $early_order_handler; $this->registration_needed = $registration_needed; + $this->card_billing_data_mode = $card_billing_data_mode; $this->logger = $logger; } @@ -204,7 +217,7 @@ class CreateOrderEndpoint implements EndpointInterface { // The cart does not have any info about payment method, so we must handle free trial here. if ( ( - CreditCardGateway::ID === $payment_method + in_array( $payment_method, array( CreditCardGateway::ID, CardButtonGateway::ID ), true ) || ( PayPalGateway::ID === $payment_method && 'card' === $funding_source ) ) && $this->is_free_trial_cart() @@ -331,18 +344,40 @@ class CreateOrderEndpoint implements EndpointInterface { private function create_paypal_order( \WC_Order $wc_order = null ): Order { assert( $this->purchase_unit instanceof PurchaseUnit ); + $funding_source = $this->parsed_request_data['funding_source'] ?? ''; + $payer = $this->payer( $this->parsed_request_data, $wc_order ); + $shipping_preference = $this->shipping_preference_factory->from_state( $this->purchase_unit, $this->parsed_request_data['context'], WC()->cart, - $this->parsed_request_data['funding_source'] ?? '' + $funding_source ); + if ( 'card' === $funding_source ) { + if ( CardBillingMode::MINIMAL_INPUT === $this->card_billing_data_mode ) { + if ( ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS === $shipping_preference ) { + if ( $payer ) { + $payer->set_address( null ); + } + } + if ( ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING === $shipping_preference ) { + if ( $payer ) { + $payer->set_name( null ); + } + } + } + + if ( CardBillingMode::NO_WC === $this->card_billing_data_mode ) { + $payer = null; + } + } + try { return $this->api_endpoint->create( array( $this->purchase_unit ), $shipping_preference, - $this->payer( $this->parsed_request_data, $wc_order ), + $payer, null, $this->payment_method() ); @@ -364,7 +399,7 @@ class CreateOrderEndpoint implements EndpointInterface { return $this->api_endpoint->create( array( $this->purchase_unit ), $shipping_preference, - $this->payer( $this->parsed_request_data, $wc_order ), + $payer, null, $this->payment_method() ); diff --git a/modules/ppcp-button/yarn.lock b/modules/ppcp-button/yarn.lock index e34a77cfb..7ffe05930 100644 --- a/modules/ppcp-button/yarn.lock +++ b/modules/ppcp-button/yarn.lock @@ -1353,6 +1353,11 @@ debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" diff --git a/modules/ppcp-session/src/Cancellation/CancelController.php b/modules/ppcp-session/src/Cancellation/CancelController.php index 51d48008c..3c23983f5 100644 --- a/modules/ppcp-session/src/Cancellation/CancelController.php +++ b/modules/ppcp-session/src/Cancellation/CancelController.php @@ -70,6 +70,10 @@ class CancelController { return; // Ignore for DCC. } + if ( 'card' === $this->session_handler->funding_source() ) { + return; // Ignore for card buttons. + } + $url = add_query_arg( array( $param_name => wp_create_nonce( $nonce ) ), wc_get_checkout_url() ); add_action( 'woocommerce_review_order_after_submit', diff --git a/modules/ppcp-subscription/services.php b/modules/ppcp-subscription/services.php index 026d26965..bf112050c 100644 --- a/modules/ppcp-subscription/services.php +++ b/modules/ppcp-subscription/services.php @@ -24,6 +24,8 @@ return array( $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); $payer_factory = $container->get( 'api.factory.payer' ); $environment = $container->get( 'onboarding.environment' ); + $settings = $container->get( 'wcgateway.settings' ); + $authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' ); return new RenewalHandler( $logger, $repository, @@ -31,7 +33,9 @@ return array( $purchase_unit_factory, $container->get( 'api.factory.shipping-preference' ), $payer_factory, - $environment + $environment, + $settings, + $authorized_payments_processor ); }, 'subscription.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository { diff --git a/modules/ppcp-subscription/src/RenewalHandler.php b/modules/ppcp-subscription/src/RenewalHandler.php index eebd76533..771a57c9d 100644 --- a/modules/ppcp-subscription/src/RenewalHandler.php +++ b/modules/ppcp-subscription/src/RenewalHandler.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Subscription; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; @@ -17,10 +18,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; /** * Class RenewalHandler @@ -80,16 +83,32 @@ class RenewalHandler { */ protected $environment; + /** + * The settings + * + * @var Settings + */ + protected $settings; + + /** + * The processor for authorized payments. + * + * @var AuthorizedPaymentsProcessor + */ + protected $authorized_payments_processor; + /** * RenewalHandler constructor. * - * @param LoggerInterface $logger The logger. - * @param PaymentTokenRepository $repository The payment token repository. - * @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 PayerFactory $payer_factory The payer factory. - * @param Environment $environment The environment. + * @param LoggerInterface $logger The logger. + * @param PaymentTokenRepository $repository The payment token repository. + * @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 PayerFactory $payer_factory The payer factory. + * @param Environment $environment The environment. + * @param Settings $settings The Settings. + * @param AuthorizedPaymentsProcessor $authorized_payments_processor The Authorized Payments Processor. */ public function __construct( LoggerInterface $logger, @@ -98,16 +117,20 @@ class RenewalHandler { PurchaseUnitFactory $purchase_unit_factory, ShippingPreferenceFactory $shipping_preference_factory, PayerFactory $payer_factory, - Environment $environment + Environment $environment, + Settings $settings, + AuthorizedPaymentsProcessor $authorized_payments_processor ) { - $this->logger = $logger; - $this->repository = $repository; - $this->order_endpoint = $order_endpoint; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->payer_factory = $payer_factory; - $this->environment = $environment; + $this->logger = $logger; + $this->repository = $repository; + $this->order_endpoint = $order_endpoint; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->payer_factory = $payer_factory; + $this->environment = $environment; + $this->settings = $settings; + $this->authorized_payments_processor = $authorized_payments_processor; } /** @@ -179,6 +202,14 @@ class RenewalHandler { } $this->handle_new_order_status( $order, $wc_order ); + + if ( $this->capture_authorized_downloads( $order ) && AuthorizedPaymentsProcessor::SUCCESSFUL === $this->authorized_payments_processor->process( $wc_order ) ) { + $wc_order->add_order_note( + __( 'Payment successfully captured.', 'woocommerce-paypal-payments' ) + ); + $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'true' ); + $wc_order->update_status( 'completed' ); + } } /** @@ -229,4 +260,39 @@ class RenewalHandler { return current( $tokens ); } + + /** + * Returns if an order should be captured immediately. + * + * @param Order $order The PayPal order. + * + * @return bool + * @throws NotFoundException When a setting was not found. + */ + protected function capture_authorized_downloads( Order $order ): bool { + if ( + ! $this->settings->has( 'capture_for_virtual_only' ) + || ! $this->settings->get( 'capture_for_virtual_only' ) + ) { + return false; + } + + if ( $order->intent() === 'CAPTURE' ) { + return false; + } + + /** + * We fetch the order again as the authorize endpoint (from which the Order derives) + * drops the item's category, making it impossible to check, if purchase units contain + * physical goods. + */ + $order = $this->order_endpoint->order( $order->id() ); + + foreach ( $order->purchase_units() as $unit ) { + if ( $unit->contains_physical_goods() ) { + return false; + } + } + return true; + } } diff --git a/modules/ppcp-vaulting/src/PaymentTokenChecker.php b/modules/ppcp-vaulting/src/PaymentTokenChecker.php index 604c7cc37..849c7a08f 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenChecker.php +++ b/modules/ppcp-vaulting/src/PaymentTokenChecker.php @@ -16,6 +16,7 @@ use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; @@ -118,7 +119,7 @@ class PaymentTokenChecker { if ( $tokens ) { try { if ( $this->is_free_trial_order( $wc_order ) ) { - if ( CreditCardGateway::ID === $wc_order->get_payment_method() + if ( in_array( $wc_order->get_payment_method(), array( CreditCardGateway::ID, CardButtonGateway::ID ), true ) || ( PayPalGateway::ID === $wc_order->get_payment_method() && 'card' === $wc_order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) ) ) { $order = $this->order_repository->for_wc_order( $wc_order ); diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 8cff73c1d..0e2ba2fd8 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -29,6 +29,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset; use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOEndpoint; @@ -48,7 +49,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; -use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -59,11 +60,10 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; return array( - 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { + 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); $funding_source_renderer = $container->get( 'wcgateway.funding-source.renderer' ); - $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); $settings = $container->get( 'wcgateway.settings' ); $session_handler = $container->get( 'session.handler' ); $refund_processor = $container->get( 'wcgateway.processor.refunds' ); @@ -72,8 +72,6 @@ return array( $subscription_helper = $container->get( 'subscription.helper' ); $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); $payment_token_repository = $container->get( 'vaulting.repository.payment-token' ); - $payments_endpoint = $container->get( 'api.endpoint.payments' ); - $order_endpoint = $container->get( 'api.endpoint.order' ); $environment = $container->get( 'onboarding.environment' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); $api_shop_country = $container->get( 'api.shop.country' ); @@ -81,7 +79,6 @@ return array( $settings_renderer, $funding_source_renderer, $order_processor, - $authorized_payments, $settings, $session_handler, $refund_processor, @@ -91,14 +88,11 @@ return array( $page_id, $environment, $payment_token_repository, - $container->get( 'api.factory.shipping-preference' ), $logger, - $payments_endpoint, - $order_endpoint, $api_shop_country ); }, - 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { + 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); @@ -137,27 +131,43 @@ return array( $payments_endpoint ); }, - 'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways { + 'wcgateway.card-button-gateway' => static function ( ContainerInterface $container ): CardButtonGateway { + return new CardButtonGateway( + $container->get( 'wcgateway.settings.render' ), + $container->get( 'wcgateway.order-processor' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'session.handler' ), + $container->get( 'wcgateway.processor.refunds' ), + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.transaction-url-provider' ), + $container->get( 'subscription.helper' ), + $container->get( 'wcgateway.settings.allow_card_button_gateway.default' ), + $container->get( 'onboarding.environment' ), + $container->get( 'vaulting.repository.payment-token' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways { $session_handler = $container->get( 'session.handler' ); $settings = $container->get( 'wcgateway.settings' ); return new DisableGateways( $session_handler, $settings ); }, - 'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool { + 'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool { $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; return 'wc-settings' === $page && 'checkout' === $tab; }, - 'wcgateway.is-ppcp-settings-page' => static function ( ContainerInterface $container ): bool { + 'wcgateway.is-ppcp-settings-page' => static function ( ContainerInterface $container ): bool { if ( ! $container->get( 'wcgateway.is-wc-payments-page' ) ) { return false; } $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : ''; - return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID, PayUponInvoiceGateway::ID ), true ); + return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID, PayUponInvoiceGateway::ID, CardButtonGateway::ID ), true ); }, - 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string { + 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string { if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) { return ''; } @@ -168,36 +178,47 @@ return array( return $ppcp_tab ? $ppcp_tab : $section; }, - 'wcgateway.settings' => static function ( ContainerInterface $container ): Settings { + 'wcgateway.settings' => static function ( ContainerInterface $container ): Settings { return new Settings(); }, - 'wcgateway.notice.connect' => static function ( ContainerInterface $container ): ConnectAdminNotice { + 'wcgateway.notice.connect' => static function ( ContainerInterface $container ): ConnectAdminNotice { $state = $container->get( 'onboarding.state' ); $settings = $container->get( 'wcgateway.settings' ); return new ConnectAdminNotice( $state, $settings ); }, - 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): DccWithoutPayPalAdminNotice { - $state = $container->get( 'onboarding.state' ); - $settings = $container->get( 'wcgateway.settings' ); - $is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' ); - $is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' ); - return new DccWithoutPayPalAdminNotice( $state, $settings, $is_payments_page, $is_ppcp_settings_page ); + 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice { + return new GatewayWithoutPayPalAdminNotice( + CreditCardGateway::ID, + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'wcgateway.is-wc-payments-page' ), + $container->get( 'wcgateway.is-ppcp-settings-page' ) + ); }, - 'wcgateway.notice.authorize-order-action' => + 'wcgateway.notice.card-button-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice { + return new GatewayWithoutPayPalAdminNotice( + CardButtonGateway::ID, + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'wcgateway.is-wc-payments-page' ), + $container->get( 'wcgateway.is-ppcp-settings-page' ) + ); + }, + 'wcgateway.notice.authorize-order-action' => static function ( ContainerInterface $container ): AuthorizeOrderActionNotice { return new AuthorizeOrderActionNotice(); }, - 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { + 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { return new SectionsRenderer( $container->get( 'wcgateway.current-ppcp-settings-page-id' ), $container->get( 'api.shop.country' ) ); }, - 'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus { + 'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus { $settings = $container->get( 'wcgateway.settings' ); return new SettingsStatus( $settings ); }, - 'wcgateway.settings.render' => static function ( ContainerInterface $container ): SettingsRenderer { + 'wcgateway.settings.render' => static function ( ContainerInterface $container ): SettingsRenderer { $settings = $container->get( 'wcgateway.settings' ); $state = $container->get( 'onboarding.state' ); $fields = $container->get( 'wcgateway.settings.fields' ); @@ -217,7 +238,7 @@ return array( $page_id ); }, - 'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener { + 'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener { $settings = $container->get( 'wcgateway.settings' ); $fields = $container->get( 'wcgateway.settings.fields' ); $webhook_registrar = $container->get( 'webhook.registrar' ); @@ -239,7 +260,7 @@ return array( $signup_link_ids ); }, - 'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor { + 'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor { $session_handler = $container->get( 'session.handler' ); $order_endpoint = $container->get( 'api.endpoint.order' ); @@ -264,13 +285,13 @@ return array( $order_helper ); }, - 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { + 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); $payments_endpoint = $container->get( 'api.endpoint.payments' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new RefundProcessor( $order_endpoint, $payments_endpoint, $logger ); }, - 'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor { + 'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); $payments_endpoint = $container->get( 'api.endpoint.payments' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); @@ -286,23 +307,23 @@ return array( $subscription_helper ); }, - 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { + 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new RenderAuthorizeAction( $column ); }, - 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { + 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new PaymentStatusOrderDetail( $column ); }, - 'wcgateway.admin.orders-payment-status-column' => static function ( ContainerInterface $container ): OrderTablePaymentStatusColumn { + 'wcgateway.admin.orders-payment-status-column' => static function ( ContainerInterface $container ): OrderTablePaymentStatusColumn { $settings = $container->get( 'wcgateway.settings' ); return new OrderTablePaymentStatusColumn( $settings ); }, - 'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer { + 'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer { return new FeesRenderer(); }, - 'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array { + 'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array { $state = $container->get( 'onboarding.state' ); assert( $state instanceof State ); @@ -865,6 +886,40 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), + 'card_billing_data_mode' => array( + 'title' => __( 'Card billing data handling', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'desc_tip' => true, + 'description' => __( 'Using the WC form data increases convenience for the customers, but can cause issues if card details do not match the billing data in the checkout form.', 'woocommerce-paypal-payments' ), + 'default' => $container->get( 'wcgateway.settings.card_billing_data_mode.default' ), + 'options' => array( + CardBillingMode::USE_WC => __( 'Use WC checkout form data (do not show any address fields)', 'woocommerce-paypal-payments' ), + CardBillingMode::MINIMAL_INPUT => __( 'Request only name and postal code', 'woocommerce-paypal-payments' ), + CardBillingMode::NO_WC => __( 'Do not use WC checkout form data (request all address fields)', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => array( 'paypal', CardButtonGateway::ID ), + ), + 'allow_card_button_gateway' => array( + 'title' => __( 'Separate Card Button from PayPal gateway', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'desc_tip' => true, + 'label' => __( 'Enable a separate payment gateway for the branded PayPal Debit or Credit Card button.', 'woocommerce-paypal-payments' ), + 'description' => __( 'By default, the Debit or Credit Card button is displayed in the PayPal Checkout payment gateway. This setting creates a second gateway for the Card button.', 'woocommerce-paypal-payments' ), + 'default' => $container->get( 'wcgateway.settings.allow_card_button_gateway.default' ), + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), // General button styles. 'button_style_heading' => array( @@ -2078,7 +2133,7 @@ return array( return $fields; }, - 'wcgateway.all-funding-sources' => static function( ContainerInterface $container ): array { + 'wcgateway.all-funding-sources' => static function( ContainerInterface $container ): array { return array( 'card' => _x( 'Credit or debit cards', 'Name of payment method', 'woocommerce-paypal-payments' ), 'credit' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), @@ -2096,28 +2151,28 @@ return array( ); }, - 'wcgateway.checkout.address-preset' => static function( ContainerInterface $container ): CheckoutPayPalAddressPreset { + 'wcgateway.checkout.address-preset' => static function( ContainerInterface $container ): CheckoutPayPalAddressPreset { return new CheckoutPayPalAddressPreset( $container->get( 'session.handler' ) ); }, - 'wcgateway.url' => static function ( ContainerInterface $container ): string { + 'wcgateway.url' => static function ( ContainerInterface $container ): string { return plugins_url( $container->get( 'wcgateway.relative-path' ), dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'wcgateway.relative-path' => static function( ContainerInterface $container ): string { + 'wcgateway.relative-path' => static function( ContainerInterface $container ): string { return 'modules/ppcp-wc-gateway/'; }, - 'wcgateway.absolute-path' => static function( ContainerInterface $container ): string { + 'wcgateway.absolute-path' => static function( ContainerInterface $container ): string { return plugin_dir_path( dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ) . $container->get( 'wcgateway.relative-path' ); }, - 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { + 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { $gateway = $container->get( 'wcgateway.paypal-gateway' ); $endpoint = $container->get( 'api.endpoint.order' ); $prefix = $container->get( 'api.prefix' ); @@ -2128,43 +2183,43 @@ return array( ); }, - 'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string { + 'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string { return 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; }, - 'wcgateway.transaction-url-live' => static function ( ContainerInterface $container ): string { + 'wcgateway.transaction-url-live' => static function ( ContainerInterface $container ): string { return 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; }, - 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { + 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { $sandbox_url_base = $container->get( 'wcgateway.transaction-url-sandbox' ); $live_url_base = $container->get( 'wcgateway.transaction-url-live' ); return new TransactionUrlProvider( $sandbox_url_base, $live_url_base ); }, - 'wcgateway.helper.dcc-product-status' => static function ( ContainerInterface $container ) : DCCProductStatus { + 'wcgateway.helper.dcc-product-status' => static function ( ContainerInterface $container ) : DCCProductStatus { $settings = $container->get( 'wcgateway.settings' ); $partner_endpoint = $container->get( 'api.endpoint.partners' ); return new DCCProductStatus( $settings, $partner_endpoint ); }, - 'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers { + 'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers { return new MessagesDisclaimers( $container->get( 'api.shop.country' ) ); }, - 'wcgateway.funding-source.renderer' => function ( ContainerInterface $container ) : FundingSourceRenderer { + 'wcgateway.funding-source.renderer' => function ( ContainerInterface $container ) : FundingSourceRenderer { return new FundingSourceRenderer( $container->get( 'wcgateway.settings' ) ); }, - 'wcgateway.checkout-helper' => static function ( ContainerInterface $container ): CheckoutHelper { + 'wcgateway.checkout-helper' => static function ( ContainerInterface $container ): CheckoutHelper { return new CheckoutHelper(); }, - 'wcgateway.pay-upon-invoice-order-endpoint' => static function ( ContainerInterface $container ): PayUponInvoiceOrderEndpoint { + 'wcgateway.pay-upon-invoice-order-endpoint' => static function ( ContainerInterface $container ): PayUponInvoiceOrderEndpoint { return new PayUponInvoiceOrderEndpoint( $container->get( 'api.host' ), $container->get( 'api.bearer' ), @@ -2173,10 +2228,10 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'wcgateway.pay-upon-invoice-payment-source-factory' => static function ( ContainerInterface $container ): PaymentSourceFactory { + 'wcgateway.pay-upon-invoice-payment-source-factory' => static function ( ContainerInterface $container ): PaymentSourceFactory { return new PaymentSourceFactory(); }, - 'wcgateway.pay-upon-invoice-gateway' => static function ( ContainerInterface $container ): PayUponInvoiceGateway { + 'wcgateway.pay-upon-invoice-gateway' => static function ( ContainerInterface $container ): PayUponInvoiceGateway { return new PayUponInvoiceGateway( $container->get( 'wcgateway.pay-upon-invoice-order-endpoint' ), $container->get( 'api.factory.purchase-unit' ), @@ -2188,13 +2243,13 @@ return array( $container->get( 'wcgateway.checkout-helper' ) ); }, - 'wcgateway.pay-upon-invoice-fraudnet-session-id' => static function ( ContainerInterface $container ): FraudNetSessionId { + 'wcgateway.pay-upon-invoice-fraudnet-session-id' => static function ( ContainerInterface $container ): FraudNetSessionId { return new FraudNetSessionId(); }, 'wcgateway.pay-upon-invoice-fraudnet-source-website-id' => static function ( ContainerInterface $container ): FraudNetSourceWebsiteId { return new FraudNetSourceWebsiteId( $container->get( 'api.merchant_id' ) ); }, - 'wcgateway.pay-upon-invoice-fraudnet' => static function ( ContainerInterface $container ): FraudNet { + 'wcgateway.pay-upon-invoice-fraudnet' => static function ( ContainerInterface $container ): FraudNet { $session_id = $container->get( 'wcgateway.pay-upon-invoice-fraudnet-session-id' ); $source_website_id = $container->get( 'wcgateway.pay-upon-invoice-fraudnet-source-website-id' ); return new FraudNet( @@ -2202,18 +2257,18 @@ return array( (string) $source_website_id() ); }, - 'wcgateway.pay-upon-invoice-helper' => static function( ContainerInterface $container ): PayUponInvoiceHelper { + 'wcgateway.pay-upon-invoice-helper' => static function( ContainerInterface $container ): PayUponInvoiceHelper { return new PayUponInvoiceHelper( $container->get( 'wcgateway.checkout-helper' ) ); }, - 'wcgateway.pay-upon-invoice-product-status' => static function( ContainerInterface $container ): PayUponInvoiceProductStatus { + 'wcgateway.pay-upon-invoice-product-status' => static function( ContainerInterface $container ): PayUponInvoiceProductStatus { return new PayUponInvoiceProductStatus( $container->get( 'wcgateway.settings' ), $container->get( 'api.endpoint.partners' ) ); }, - 'wcgateway.pay-upon-invoice' => static function ( ContainerInterface $container ): PayUponInvoice { + 'wcgateway.pay-upon-invoice' => static function ( ContainerInterface $container ): PayUponInvoice { return new PayUponInvoice( $container->get( 'wcgateway.url' ), $container->get( 'wcgateway.pay-upon-invoice-fraudnet' ), @@ -2231,14 +2286,14 @@ return array( $container->get( 'api.factory.capture' ) ); }, - 'wcgateway.oxxo' => static function( ContainerInterface $container ): OXXO { + 'wcgateway.oxxo' => static function( ContainerInterface $container ): OXXO { return new OXXO( $container->get( 'wcgateway.checkout-helper' ), $container->get( 'wcgateway.url' ), $container->get( 'ppcp.asset-version' ) ); }, - 'wcgateway.oxxo-gateway' => static function( ContainerInterface $container ): OXXOGateway { + 'wcgateway.oxxo-gateway' => static function( ContainerInterface $container ): OXXOGateway { return new OXXOGateway( $container->get( 'api.endpoint.order' ), $container->get( 'api.factory.purchase-unit' ), @@ -2246,7 +2301,7 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'wcgateway.endpoint.oxxo' => static function ( ContainerInterface $container ): OXXOEndpoint { + 'wcgateway.endpoint.oxxo' => static function ( ContainerInterface $container ): OXXOEndpoint { return new OXXOEndpoint( $container->get( 'button.request-data' ), $container->get( 'api.endpoint.order' ), @@ -2255,7 +2310,7 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { + 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { $settings = $container->get( 'wcgateway.settings' ); /** @@ -2267,7 +2322,7 @@ return array( ); }, - 'wcgateway.helper.vaulting-scope' => static function ( ContainerInterface $container ): bool { + 'wcgateway.helper.vaulting-scope' => static function ( ContainerInterface $container ): bool { try { $token = $container->get( 'api.bearer' )->bearer(); return $token->vaulting_available(); @@ -2276,7 +2331,7 @@ return array( } }, - 'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string { + 'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string { $vaulting_label = __( 'Enable saved cards and subscription features on your store.', 'woocommerce-paypal-payments' ); if ( ! $container->get( 'wcgateway.helper.vaulting-scope' ) ) { @@ -2298,7 +2353,7 @@ return array( return $vaulting_label; }, - 'wcgateway.settings.fields.pay-later-label' => static function ( ContainerInterface $container ): string { + 'wcgateway.settings.fields.pay-later-label' => static function ( ContainerInterface $container ): string { $pay_later_label = '%s'; $pay_later_label .= ''; $pay_later_label .= __( "You have PayPal vaulting enabled, that's why Pay Later Messaging options are unavailable now. You cannot use both features at the same time.", 'woocommerce-paypal-payments' ); @@ -2306,4 +2361,28 @@ return array( return $pay_later_label; }, + + 'wcgateway.settings.card_billing_data_mode.default' => static function ( ContainerInterface $container ): string { + return $container->get( 'api.shop.is-latin-america' ) ? CardBillingMode::MINIMAL_INPUT : CardBillingMode::USE_WC; + }, + 'wcgateway.settings.card_billing_data_mode' => static function ( ContainerInterface $container ): string { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof ContainerInterface ); + + return $settings->has( 'card_billing_data_mode' ) ? + (string) $settings->get( 'card_billing_data_mode' ) : + $container->get( 'wcgateway.settings.card_billing_data_mode.default' ); + }, + + 'wcgateway.settings.allow_card_button_gateway.default' => static function ( ContainerInterface $container ): bool { + return $container->get( 'api.shop.is-latin-america' ); + }, + 'wcgateway.settings.allow_card_button_gateway' => static function ( ContainerInterface $container ): bool { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof ContainerInterface ); + + return $settings->has( 'allow_card_button_gateway' ) ? + (bool) $settings->get( 'allow_card_button_gateway' ) : + $container->get( 'wcgateway.settings.allow_card_button_gateway.default' ); + }, ); diff --git a/modules/ppcp-wc-gateway/src/CardBillingMode.php b/modules/ppcp-wc-gateway/src/CardBillingMode.php new file mode 100644 index 000000000..300b186ca --- /dev/null +++ b/modules/ppcp-wc-gateway/src/CardBillingMode.php @@ -0,0 +1,19 @@ +disable_both_gateways() ) { + if ( $this->disable_all_gateways() ) { unset( $methods[ PayPalGateway::ID ] ); unset( $methods[ CreditCardGateway::ID ] ); + unset( $methods[ CardButtonGateway::ID ] ); return $methods; } @@ -77,21 +79,15 @@ class DisableGateways { return $methods; } - if ( $this->is_credit_card() ) { - return array( - CreditCardGateway::ID => $methods[ CreditCardGateway::ID ], - PayPalGateway::ID => $methods[ PayPalGateway::ID ], - ); - } return array( PayPalGateway::ID => $methods[ PayPalGateway::ID ] ); } /** - * Whether both gateways should be disabled or not. + * Whether all gateways should be disabled or not. * * @return bool */ - private function disable_both_gateways() : bool { + private function disable_all_gateways() : bool { if ( ! $this->settings->has( 'enabled' ) || ! $this->settings->get( 'enabled' ) ) { return true; } @@ -110,22 +106,20 @@ class DisableGateways { * @return bool */ private function needs_to_disable_gateways(): bool { - return $this->session_handler->order() !== null; - } - - /** - * Whether the current PayPal session is done via DCC payment. - * - * @return bool - */ - private function is_credit_card(): bool { $order = $this->session_handler->order(); if ( ! $order ) { return false; } - if ( ! $order->payment_source() || ! $order->payment_source()->card() ) { - return false; + + $source = $order->payment_source(); + if ( $source && $source->card() ) { + return false; // DCC. } + + if ( 'card' === $this->session_handler->funding_source() ) { + return false; // Card buttons. + } + return true; } } diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 0fdc07954..57c29c2a4 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -52,7 +52,7 @@ class ReturnUrlEndpoint { /** * Handles the incoming request. */ - public function handle_request() { + public function handle_request(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['token'] ) ) { diff --git a/modules/ppcp-wc-gateway/src/Exception/GatewayGenericException.php b/modules/ppcp-wc-gateway/src/Exception/GatewayGenericException.php new file mode 100644 index 000000000..cbf157b03 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Exception/GatewayGenericException.php @@ -0,0 +1,32 @@ +getCode() : 0, + $inner + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php new file mode 100644 index 000000000..45aff4ce6 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php @@ -0,0 +1,364 @@ +id = self::ID; + $this->settings_renderer = $settings_renderer; + $this->order_processor = $order_processor; + $this->config = $config; + $this->session_handler = $session_handler; + $this->refund_processor = $refund_processor; + $this->state = $state; + $this->transaction_url_provider = $transaction_url_provider; + $this->subscription_helper = $subscription_helper; + $this->default_enabled = $default_enabled; + $this->environment = $environment; + $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; + $this->payment_token_repository = $payment_token_repository; + $this->logger = $logger; + + if ( $this->onboarded ) { + $this->supports = array( 'refunds' ); + } + if ( + defined( 'PPCP_FLAG_SUBSCRIPTION' ) + && PPCP_FLAG_SUBSCRIPTION + && $this->gateways_enabled() + && $this->vault_setting_enabled() + ) { + $this->supports = array( + 'refunds', + 'products', + 'subscriptions', + 'subscription_cancellation', + 'subscription_suspension', + 'subscription_reactivation', + 'subscription_amount_changes', + 'subscription_date_changes', + 'subscription_payment_method_change', + 'subscription_payment_method_change_customer', + 'subscription_payment_method_change_admin', + 'multiple_subscriptions', + ); + } + + $this->method_title = __( 'PayPal Card Button', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'The separate payment gateway with the card button. If disabled, the button is included in the PayPal gateway.', 'woocommerce-paypal-payments' ); + $this->title = $this->get_option( 'title', __( 'Debit & Credit Cards', 'woocommerce-paypal-payments' ) ); + $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', + ) + ); + } + + /** + * Whether the Gateway needs to be setup. + * + * @return bool + */ + public function needs_setup(): bool { + return ! $this->onboarded; + } + + /** + * Initializes the form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal Card Button', 'woocommerce-paypal-payments' ), + 'default' => $this->default_enabled ? 'yes' : 'no', + 'desc_tip' => true, + 'description' => __( 'Enable/Disable the separate payment gateway with the card button.', 'woocommerce-paypal-payments' ), + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->title, + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->description, + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'ppcp' => array( + 'type' => 'ppcp', + ), + ); + } + + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + /** + * If customer has chosen change Subscription payment. + */ + if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { + $saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING ); + if ( $saved_paypal_payment ) { + update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment ); + + return $this->handle_payment_success( $wc_order ); + } + } + + /** + * If the WC_Order is paid through the approved webhook. + */ + //phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { + return $this->handle_payment_success( $wc_order ); + } + //phpcs:enable WordPress.Security.NonceVerification.Recommended + + try { + if ( ! $this->order_processor->process( $wc_order ) ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + $this->order_processor->last_error() + ) + ); + } + + if ( $this->subscription_helper->has_subscription( $order_id ) ) { + $this->schedule_saved_payment_check( $order_id, $wc_order->get_customer_id() ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalApiException $error ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + + /** + * Process refund. + * + * If the gateway declares 'refunds' support, this will allow it to refund. + * a passed in amount. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * @return boolean True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $order = wc_get_order( $order_id ); + if ( ! is_a( $order, \WC_Order::class ) ) { + return false; + } + return $this->refund_processor->process( $order, (float) $amount, (string) $reason ); + } + + /** + * 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 ); + } + + /** + * Returns the settings renderer. + * + * @return SettingsRenderer + */ + protected function settings_renderer(): SettingsRenderer { + return $this->settings_renderer; + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php index b11116878..4b97120bc 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -9,20 +9,30 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use Exception; use Psr\Log\LoggerInterface; +use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; +use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; +use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use Psr\Container\ContainerInterface; @@ -31,7 +41,8 @@ use Psr\Container\ContainerInterface; */ class CreditCardGateway extends \WC_Payment_Gateway_CC { - use ProcessPaymentTrait; + use ProcessPaymentTrait, OrderMetaTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait, FreeTrialHandlerTrait, + GatewaySettingsRendererTrait; const ID = 'ppcp-credit-card-gateway'; @@ -203,15 +214,25 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { Environment $environment, PaymentsEndpoint $payments_endpoint ) { - $this->id = self::ID; + $this->settings_renderer = $settings_renderer; $this->order_processor = $order_processor; $this->authorized_payments_processor = $authorized_payments_processor; - $this->settings_renderer = $settings_renderer; $this->config = $config; + $this->module_url = $module_url; $this->session_handler = $session_handler; $this->refund_processor = $refund_processor; + $this->state = $state; + $this->transaction_url_provider = $transaction_url_provider; + $this->payment_token_repository = $payment_token_repository; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->payer_factory = $payer_factory; + $this->order_endpoint = $order_endpoint; + $this->subscription_helper = $subscription_helper; + $this->logger = $logger; $this->environment = $environment; + $this->payments_endpoint = $payments_endpoint; if ( $state->current_state() === State::STATE_ONBOARDED ) { $this->supports = array( 'refunds' ); @@ -261,18 +282,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { 'process_admin_options', ) ); - - $this->module_url = $module_url; - $this->payment_token_repository = $payment_token_repository; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->payer_factory = $payer_factory; - $this->order_endpoint = $order_endpoint; - $this->transaction_url_provider = $transaction_url_provider; - $this->subscription_helper = $subscription_helper; - $this->logger = $logger; - $this->payments_endpoint = $payments_endpoint; - $this->state = $state; } /** @@ -295,20 +304,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { remove_action( 'gettext', 'replace_credit_card_cvv_label' ); } - /** - * Renders the settings. - * - * @return string - */ - public function generate_ppcp_html(): string { - - ob_start(); - $this->settings_renderer->render(); - $content = ob_get_contents(); - ob_end_clean(); - return $content; - } - /** * Replace WooCommerce credit card field label. * @@ -409,6 +404,158 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { return $this->is_enabled(); } + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + /** + * If customer has chosen a saved credit card payment. + */ + $saved_credit_card = filter_input( INPUT_POST, 'saved_credit_card', FILTER_SANITIZE_STRING ); + $change_payment = filter_input( INPUT_POST, 'woocommerce_change_payment', FILTER_SANITIZE_STRING ); + if ( $saved_credit_card && ! isset( $change_payment ) ) { + + $user_id = (int) $wc_order->get_customer_id(); + $customer = new \WC_Customer( $user_id ); + $tokens = $this->payment_token_repository->all_for_user_id( (int) $customer->get_id() ); + + $selected_token = null; + foreach ( $tokens as $token ) { + if ( $token->id() === $saved_credit_card ) { + $selected_token = $token; + break; + } + } + + if ( ! $selected_token ) { + return $this->handle_payment_failure( + $wc_order, + new GatewayGenericException( new Exception( 'Saved card token not found.' ) ) + ); + } + + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); + $payer = $this->payer_factory->from_customer( $customer ); + + $shipping_preference = $this->shipping_preference_factory->from_state( + $purchase_unit, + '' + ); + + try { + $order = $this->order_endpoint->create( + array( $purchase_unit ), + $shipping_preference, + $payer, + $selected_token + ); + + $this->add_paypal_meta( $wc_order, $order, $this->environment ); + + if ( ! $order->status()->is( OrderStatus::COMPLETED ) ) { + return $this->handle_payment_failure( + $wc_order, + new GatewayGenericException( new Exception( "Unexpected status for order {$order->id()} using a saved card: {$order->status()->name()}." ) ) + ); + } + + if ( ! in_array( + $order->intent(), + array( 'CAPTURE', 'AUTHORIZE' ), + true + ) ) { + return $this->handle_payment_failure( + $wc_order, + new GatewayGenericException( new Exception( "Could neither capture nor authorize order {$order->id()} using a saved card. Status: {$order->status()->name()}. Intent: {$order->intent()}." ) ) + ); + } + + if ( $order->intent() === 'AUTHORIZE' ) { + $order = $this->order_endpoint->authorize( $order ); + + $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, '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->is_free_trial_order( $wc_order ) ) { + $this->authorized_payments_processor->void_authorizations( $order ); + $wc_order->payment_complete(); + } elseif ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) { + $this->authorized_payments_processor->capture_authorized_payment( $wc_order ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + + /** + * If customer has chosen change Subscription payment. + */ + if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { + if ( $saved_credit_card ) { + update_post_meta( $order_id, 'payment_token_id', $saved_credit_card ); + + return $this->handle_payment_success( $wc_order ); + } + } + + /** + * If the WC_Order is paid through the approved webhook. + */ + //phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { + return $this->handle_payment_success( $wc_order ); + } + //phpcs:enable WordPress.Security.NonceVerification.Recommended + + try { + if ( ! $this->order_processor->process( $wc_order ) ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + $this->order_processor->last_error() + ) + ); + } + + if ( $this->subscription_helper->has_subscription( $order_id ) ) { + $this->schedule_saved_payment_check( $order_id, $wc_order->get_customer_id() ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalApiException $error ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } /** * Process refund. @@ -500,11 +647,11 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { } /** - * Returns the environment. + * Returns the settings renderer. * - * @return Environment + * @return SettingsRenderer */ - protected function environment(): Environment { - return $this->environment; + protected function settings_renderer(): SettingsRenderer { + return $this->settings_renderer; } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/GatewaySettingsRendererTrait.php b/modules/ppcp-wc-gateway/src/Gateway/GatewaySettingsRendererTrait.php new file mode 100644 index 000000000..804ea5fdd --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/GatewaySettingsRendererTrait.php @@ -0,0 +1,37 @@ +settings_renderer()->render(); + $content = ob_get_contents(); + ob_end_clean(); + return $content; + } + + /** + * Returns the settings renderer. + * + * @return SettingsRenderer + */ + abstract protected function settings_renderer(): SettingsRenderer; +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/Messages.php b/modules/ppcp-wc-gateway/src/Gateway/Messages.php new file mode 100644 index 000000000..ea5124578 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/Messages.php @@ -0,0 +1,27 @@ +id = self::ID; - $this->order_processor = $order_processor; - $this->authorized_payments_processor = $authorized_payments_processor; - $this->settings_renderer = $settings_renderer; - $this->funding_source_renderer = $funding_source_renderer; - $this->config = $config; - $this->session_handler = $session_handler; - $this->refund_processor = $refund_processor; - $this->transaction_url_provider = $transaction_url_provider; - $this->page_id = $page_id; - $this->environment = $environment; - $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; - $this->id = self::ID; - $this->order_processor = $order_processor; - $this->authorized_payments = $authorized_payments_processor; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->settings_renderer = $settings_renderer; - $this->config = $config; - $this->session_handler = $session_handler; - $this->refund_processor = $refund_processor; - $this->transaction_url_provider = $transaction_url_provider; - $this->page_id = $page_id; - $this->environment = $environment; - $this->logger = $logger; + $this->id = self::ID; + $this->settings_renderer = $settings_renderer; + $this->funding_source_renderer = $funding_source_renderer; + $this->order_processor = $order_processor; + $this->config = $config; + $this->session_handler = $session_handler; + $this->refund_processor = $refund_processor; + $this->state = $state; + $this->transaction_url_provider = $transaction_url_provider; + $this->subscription_helper = $subscription_helper; + $this->page_id = $page_id; + $this->environment = $environment; + $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; + $this->payment_token_repository = $payment_token_repository; + $this->logger = $logger; + $this->api_shop_country = $api_shop_country; if ( $this->onboarded ) { $this->supports = array( 'refunds' ); @@ -291,13 +249,6 @@ class PayPalGateway extends \WC_Payment_Gateway { 'process_admin_options', ) ); - $this->subscription_helper = $subscription_helper; - $this->payment_token_repository = $payment_token_repository; - $this->logger = $logger; - $this->payments_endpoint = $payments_endpoint; - $this->order_endpoint = $order_endpoint; - $this->state = $state; - $this->api_shop_country = $api_shop_country; } /** @@ -306,7 +257,6 @@ class PayPalGateway extends \WC_Payment_Gateway { * @return bool */ public function needs_setup(): bool { - return ! $this->onboarded; } @@ -334,20 +284,6 @@ class PayPalGateway extends \WC_Payment_Gateway { } } - /** - * Renders the settings. - * - * @return string - */ - public function generate_ppcp_html(): string { - - ob_start(); - $this->settings_renderer->render( false ); - $content = ob_get_contents(); - ob_end_clean(); - return $content; - } - /** * Defines the method title. If we are on the credit card tab in the settings, we want to change this. * @@ -450,6 +386,118 @@ class PayPalGateway extends \WC_Payment_Gateway { } // phpcs:enable WordPress.Security.NonceVerification.Recommended + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + $funding_source = filter_input( INPUT_POST, 'ppcp-funding-source', FILTER_SANITIZE_STRING ); + + if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) ) { + $user_id = (int) $wc_order->get_customer_id(); + $tokens = $this->payment_token_repository->all_for_user_id( $user_id ); + if ( ! array_filter( + $tokens, + function ( PaymentToken $token ): bool { + return isset( $token->source()->paypal ); + } + ) ) { + return $this->handle_payment_failure( $wc_order, new Exception( 'No saved PayPal account.' ) ); + } + + $wc_order->payment_complete(); + + return $this->handle_payment_success( $wc_order ); + } + + /** + * If customer has chosen change Subscription payment. + */ + if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { + $saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING ); + if ( $saved_paypal_payment ) { + update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment ); + + return $this->handle_payment_success( $wc_order ); + } + } + + /** + * If the WC_Order is paid through the approved webhook. + */ + //phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { + return $this->handle_payment_success( $wc_order ); + } + //phpcs:enable WordPress.Security.NonceVerification.Recommended + + try { + if ( ! $this->order_processor->process( $wc_order ) ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + $this->order_processor->last_error() + ) + ); + } + + if ( $this->subscription_helper->has_subscription( $order_id ) ) { + $this->schedule_saved_payment_check( $order_id, $wc_order->get_customer_id() ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalApiException $error ) { + if ( $error->has_detail( 'INSTRUMENT_DECLINED' ) ) { + $wc_order->update_status( + 'failed', + __( 'Instrument declined. ', 'woocommerce-paypal-payments' ) . $error->details()[0]->description ?? '' + ); + + $this->session_handler->increment_insufficient_funding_tries(); + if ( $this->session_handler->insufficient_funding_tries() >= 3 ) { + return $this->handle_payment_failure( + null, + new Exception( + __( 'Please use a different payment method.', 'woocommerce-paypal-payments' ), + $error->getCode(), + $error + ) + ); + } + + $host = $this->config->has( 'sandbox_on' ) && $this->config->get( 'sandbox_on' ) ? + 'https://www.sandbox.paypal.com/' : 'https://www.paypal.com/'; + $url = $host . 'checkoutnow?token=' . $this->session_handler->order()->id(); + return array( + 'result' => 'success', + 'redirect' => $url, + ); + } + + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + /** * Process refund. * @@ -503,11 +551,11 @@ class PayPalGateway extends \WC_Payment_Gateway { } /** - * Returns the environment. + * Returns the settings renderer. * - * @return Environment + * @return SettingsRenderer */ - protected function environment(): Environment { - return $this->environment; + protected function settings_renderer(): SettingsRenderer { + return $this->settings_renderer; } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index 10e0e1313..f78871d1a 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -10,279 +10,14 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Exception; -use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; -use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; -use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; -use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\Onboarding\Environment; -use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; -use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; -use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; -use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; -use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use Throwable; +use WC_Order; +use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; /** * Trait ProcessPaymentTrait */ trait ProcessPaymentTrait { - - use OrderMetaTrait, PaymentsStatusHandlingTrait, TransactionIdHandlingTrait, FreeTrialHandlerTrait; - - /** - * Process a payment for an WooCommerce order. - * - * @param int $order_id The WooCommerce order id. - * - * @return array - * - * @throws RuntimeException When processing payment fails. - */ - public function process_payment( $order_id ) { - - $failure_data = array( - 'result' => 'failure', - 'redirect' => wc_get_checkout_url(), - ); - - $wc_order = wc_get_order( $order_id ); - if ( ! is_a( $wc_order, \WC_Order::class ) ) { - wc_add_notice( - __( 'Couldn\'t find order to process', 'woocommerce-paypal-payments' ), - 'error' - ); - - return $failure_data; - } - - $payment_method = filter_input( INPUT_POST, 'payment_method', FILTER_SANITIZE_STRING ); - $funding_source = filter_input( INPUT_POST, 'ppcp-funding-source', FILTER_SANITIZE_STRING ); - - /** - * If customer has chosen a saved credit card payment. - */ - $saved_credit_card = filter_input( INPUT_POST, 'saved_credit_card', FILTER_SANITIZE_STRING ); - $change_payment = filter_input( INPUT_POST, 'woocommerce_change_payment', FILTER_SANITIZE_STRING ); - if ( CreditCardGateway::ID === $payment_method && $saved_credit_card && ! isset( $change_payment ) ) { - - $user_id = (int) $wc_order->get_customer_id(); - $customer = new \WC_Customer( $user_id ); - $tokens = $this->payment_token_repository->all_for_user_id( (int) $customer->get_id() ); - - $selected_token = null; - foreach ( $tokens as $token ) { - if ( $token->id() === $saved_credit_card ) { - $selected_token = $token; - break; - } - } - - if ( ! $selected_token ) { - return null; - } - - $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); - $payer = $this->payer_factory->from_customer( $customer ); - - $shipping_preference = $this->shipping_preference_factory->from_state( - $purchase_unit, - '' - ); - - try { - $order = $this->order_endpoint->create( - array( $purchase_unit ), - $shipping_preference, - $payer, - $selected_token - ); - - $this->add_paypal_meta( $wc_order, $order, $this->environment() ); - - if ( ! $order->status()->is( OrderStatus::COMPLETED ) ) { - $this->logger->warning( "Unexpected status for order {$order->id()} using a saved credit card: " . $order->status()->name() ); - return null; - } - - if ( ! in_array( - $order->intent(), - array( 'CAPTURE', 'AUTHORIZE' ), - true - ) ) { - $this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() ); - return null; - } - - if ( $order->intent() === 'AUTHORIZE' ) { - $order = $this->order_endpoint->authorize( $order ); - - $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, '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->is_free_trial_order( $wc_order ) ) { - $this->authorized_payments_processor->void_authorizations( $order ); - $wc_order->payment_complete(); - } elseif ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) { - $this->authorized_payments_processor->capture_authorized_payment( $wc_order ); - } - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } catch ( RuntimeException $error ) { - $this->handle_failure( $wc_order, $error ); - return null; - } - } - - if ( PayPalGateway::ID === $payment_method && 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) ) { - $user_id = (int) $wc_order->get_customer_id(); - $tokens = $this->payment_token_repository->all_for_user_id( $user_id ); - if ( ! array_filter( - $tokens, - function ( PaymentToken $token ): bool { - return isset( $token->source()->paypal ); - } - ) ) { - $this->handle_failure( $wc_order, new Exception( 'No saved PayPal account.' ) ); - return null; - } - - $wc_order->payment_complete(); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - - /** - * If customer has chosen change Subscription payment. - */ - if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { - if ( 'ppcp-credit-card-gateway' === $this->id && $saved_credit_card ) { - update_post_meta( $order_id, 'payment_token_id', $saved_credit_card ); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - - $saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING ); - if ( 'ppcp-gateway' === $this->id && $saved_paypal_payment ) { - update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment ); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - } - - /** - * If the WC_Order is payed through the approved webhook. - */ - //phpcs:disable WordPress.Security.NonceVerification.Recommended - if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - //phpcs:enable WordPress.Security.NonceVerification.Recommended - - try { - if ( $this->order_processor->process( $wc_order ) ) { - if ( $this->subscription_helper->has_subscription( $order_id ) ) { - as_schedule_single_action( - time() + ( 1 * MINUTE_IN_SECONDS ), - 'woocommerce_paypal_payments_check_saved_payment', - array( - 'order_id' => $order_id, - 'customer_id' => $wc_order->get_customer_id(), - 'intent' => $this->config->has( 'intent' ) ? $this->config->get( 'intent' ) : '', - ) - ); - } - - WC()->cart->empty_cart(); - $this->session_handler->destroy_session_data(); - - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - } catch ( PayPalApiException $error ) { - if ( $error->has_detail( 'INSTRUMENT_DECLINED' ) ) { - $wc_order->update_status( - 'failed', - __( 'Instrument declined. ', 'woocommerce-paypal-payments' ) . $error->details()[0]->description ?? '' - ); - - $this->session_handler->increment_insufficient_funding_tries(); - $host = $this->config->has( 'sandbox_on' ) && $this->config->get( 'sandbox_on' ) ? - 'https://www.sandbox.paypal.com/' : 'https://www.paypal.com/'; - $url = $host . 'checkoutnow?token=' . $this->session_handler->order()->id(); - if ( $this->session_handler->insufficient_funding_tries() >= 3 ) { - $this->session_handler->destroy_session_data(); - wc_add_notice( - __( 'Please use a different payment method.', 'woocommerce-paypal-payments' ), - 'error' - ); - return $failure_data; - } - return array( - 'result' => 'success', - 'redirect' => $url, - ); - } - - $error_message = $error->getMessage(); - if ( $error->issues() ) { - $error_message = implode( - array_map( - function( $issue ) { - return $issue->issue . ' ' . $issue->description . '
'; - }, - $error->issues() - ) - ); - } - wc_add_notice( $error_message, 'error' ); - - $this->session_handler->destroy_session_data(); - } catch ( RuntimeException $error ) { - $this->handle_failure( $wc_order, $error ); - return $failure_data; - } - - wc_add_notice( - $this->order_processor->last_error(), - 'error' - ); - - $wc_order->update_status( - 'failed', - __( 'Could not process order. ', 'woocommerce-paypal-payments' ) . $this->order_processor->last_error() - ); - - return $failure_data; - } - /** * Checks if PayPal or Credit Card gateways are enabled. * @@ -311,29 +46,86 @@ trait ProcessPaymentTrait { return false; } + /** + * Scheduled the vaulted payment check. + * + * @param int $wc_order_id The WC order ID. + * @param int $customer_id The customer ID. + */ + protected function schedule_saved_payment_check( int $wc_order_id, int $customer_id ): void { + as_schedule_single_action( + time() + ( 1 * MINUTE_IN_SECONDS ), + 'woocommerce_paypal_payments_check_saved_payment', + array( + 'order_id' => $wc_order_id, + 'customer_id' => $customer_id, + 'intent' => $this->config->has( 'intent' ) ? $this->config->get( 'intent' ) : '', + ) + ); + } + /** * Handles the payment failure. * - * @param \WC_Order $wc_order The order. - * @param Exception $error The error causing the failure. + * @param WC_Order|null $wc_order The order. + * @param Exception $error The error causing the failure. + * @return array The data that can be returned by the gateway process_payment method. */ - protected function handle_failure( \WC_Order $wc_order, Exception $error ): void { - $this->logger->error( 'Payment failed: ' . $error->getMessage() ); + protected function handle_payment_failure( ?WC_Order $wc_order, Exception $error ): array { + $this->logger->error( 'Payment failed: ' . $this->format_exception( $error ) ); - $wc_order->update_status( - 'failed', - __( 'Could not process order. ', 'woocommerce-paypal-payments' ) . $error->getMessage() - ); + if ( $wc_order ) { + $wc_order->update_status( + 'failed', + $this->format_exception( $error ) + ); + } $this->session_handler->destroy_session_data(); wc_add_notice( $error->getMessage(), 'error' ); + + return array( + 'result' => 'failure', + 'redirect' => wc_get_checkout_url(), + ); } /** - * Returns the environment. + * Handles the payment completion. * - * @return Environment + * @param WC_Order|null $wc_order The order. + * @param string|null $url The redirect URL. + * @return array The data that can be returned by the gateway process_payment method. */ - abstract protected function environment(): Environment; + protected function handle_payment_success( ?WC_Order $wc_order, string $url = null ): array { + if ( ! $url ) { + $url = $this->get_return_url( $wc_order ); + } + + $this->session_handler->destroy_session_data(); + + return array( + 'result' => 'success', + 'redirect' => $url, + ); + } + + /** + * Outputs the exception, including the inner exception. + * + * @param Throwable $exception The exception to format. + * @return string + */ + protected function format_exception( Throwable $exception ): string { + $output = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine(); + $prev = $exception->getPrevious(); + if ( ! $prev ) { + return $output; + } + if ( $exception instanceof GatewayGenericException ) { + $output = ''; + } + return $output . ' ' . $this->format_exception( $prev ); + } } diff --git a/modules/ppcp-wc-gateway/src/Helper/CheckoutHelper.php b/modules/ppcp-wc-gateway/src/Helper/CheckoutHelper.php index a6ef08cd6..977144024 100644 --- a/modules/ppcp-wc-gateway/src/Helper/CheckoutHelper.php +++ b/modules/ppcp-wc-gateway/src/Helper/CheckoutHelper.php @@ -98,6 +98,9 @@ class CheckoutHelper { if ( $date_time && time() < strtotime( '+18 years', $date_time ) ) { return false; } + if ( $date_time < strtotime( '-100 years', time() ) ) { + return false; + } return true; } diff --git a/modules/ppcp-wc-gateway/src/Notice/DccWithoutPayPalAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php similarity index 57% rename from modules/ppcp-wc-gateway/src/Notice/DccWithoutPayPalAdminNotice.php rename to modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php index 2343c9e00..171f50280 100644 --- a/modules/ppcp-wc-gateway/src/Notice/DccWithoutPayPalAdminNotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php @@ -1,6 +1,6 @@ id = $id; $this->state = $state; $this->settings = $settings; $this->is_payments_page = $is_payments_page; @@ -76,12 +86,20 @@ class DccWithoutPayPalAdminNotice { return null; } + $gateway = $this->get_gateway(); + if ( ! $gateway ) { + return null; + } + + $name = $gateway->get_method_title(); + $message = sprintf( - /* translators: %1$s the gateway name. */ + /* translators: %1$s the gateway name, %2$s URL. */ __( - 'PayPal Card Processing cannot be used without the PayPal gateway. Enable the PayPal Gateway.', + '%1$s cannot be used without the PayPal gateway. Enable the PayPal gateway.', 'woocommerce-paypal-payments' ), + $name, admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) ); return new Message( $message, 'warning' ); @@ -93,9 +111,29 @@ class DccWithoutPayPalAdminNotice { * @return bool */ protected function should_display(): bool { - return State::STATE_ONBOARDED === $this->state->current_state() - && ( $this->is_payments_page || $this->is_ppcp_settings_page ) - && ( $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) - && ( ! $this->settings->has( 'enabled' ) || ! $this->settings->get( 'enabled' ) ); + if ( State::STATE_ONBOARDED !== $this->state->current_state() || + ( ! $this->is_payments_page && ! $this->is_ppcp_settings_page ) ) { + return false; + } + if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) { + return false; + } + + $gateway = $this->get_gateway(); + + return $gateway && wc_string_to_bool( $gateway->get_option( 'enabled' ) ); + } + + /** + * Returns the gateway object or null. + * + * @return WC_Payment_Gateway|null + */ + protected function get_gateway(): ?WC_Payment_Gateway { + $gateways = WC()->payment_gateways->payment_gateways(); + if ( ! isset( $gateways[ $this->id ] ) ) { + return null; + } + return $gateways[ $this->id ]; } } diff --git a/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php b/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php index f3b41fdfb..ea301362b 100644 --- a/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php +++ b/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; @@ -34,6 +35,7 @@ trait PageMatcherTrait { $gateway_page_id_map = array( PayPalGateway::ID => 'paypal', CreditCardGateway::ID => 'dcc', // TODO: consider using just the gateway ID for PayPal and DCC too. + CardButtonGateway::ID => CardButtonGateway::ID, WebhooksStatusPage::ID => WebhooksStatusPage::ID, ); return array_key_exists( $current_page_id, $gateway_page_id_map ) diff --git a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php index 24faa5201..631a078c7 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php @@ -9,6 +9,7 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; @@ -58,7 +59,7 @@ class SectionsRenderer { /** * Renders the Sections tab. */ - public function render() { + public function render(): void { if ( ! $this->should_render() ) { return; } @@ -66,6 +67,7 @@ class SectionsRenderer { $sections = array( PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), + CardButtonGateway::ID => __( 'PayPal Card Button', 'woocommerce-paypal-payments' ), PayUponInvoiceGateway::ID => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), WebhooksStatusPage::ID => __( 'Webhooks Status', 'woocommerce-paypal-payments' ), ); @@ -80,8 +82,8 @@ class SectionsRenderer { foreach ( $sections as $id => $label ) { $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&' . self::KEY . '=' . $id ); - if ( PayUponInvoiceGateway::ID === $id ) { - $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-pay-upon-invoice-gateway' ); + if ( in_array( $id, array( PayUponInvoiceGateway::ID, CardButtonGateway::ID ), true ) ) { + $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . $id ); } echo '
  • ' . esc_html( $label ) . ' ' . ( end( $array_keys ) === $id ? '' : '|' ) . '
  • '; } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 12136089c..cb69b3fb7 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -29,7 +29,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; -use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -164,11 +164,15 @@ class WCGatewayModule implements ModuleInterface { $notices[] = $connect_message; } - $dcc_without_paypal_notice = $c->get( 'wcgateway.notice.dcc-without-paypal' ); - assert( $dcc_without_paypal_notice instanceof DccWithoutPayPalAdminNotice ); - $dcc_without_paypal_message = $dcc_without_paypal_notice->message(); - if ( $dcc_without_paypal_message ) { - $notices[] = $dcc_without_paypal_message; + foreach ( array( + $c->get( 'wcgateway.notice.dcc-without-paypal' ), + $c->get( 'wcgateway.notice.card-button-without-paypal' ), + ) as $gateway_without_paypal_notice ) { + assert( $gateway_without_paypal_notice instanceof GatewayWithoutPayPalAdminNotice ); + $message = $gateway_without_paypal_notice->message(); + if ( $message ) { + $notices[] = $message; + } } $authorize_order_action = $c->get( 'wcgateway.notice.authorize-order-action' ); @@ -294,6 +298,10 @@ class WCGatewayModule implements ModuleInterface { $methods[] = $container->get( 'wcgateway.credit-card-gateway' ); } + if ( $container->get( 'wcgateway.settings.allow_card_button_gateway' ) ) { + $methods[] = $container->get( 'wcgateway.card-button-gateway' ); + } + if ( 'DE' === $container->get( 'api.shop.country' ) && 'EUR' === $container->get( 'api.shop.currency' ) ) { $methods[] = $container->get( 'wcgateway.pay-upon-invoice-gateway' ); } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php index 7a4db8bf1..d879a2345 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php @@ -46,7 +46,7 @@ class PaymentCaptureRefunded implements RequestHandler { * @return string[] */ public function event_types(): array { - return array( 'PAYMENT.CAPTURE.REFUNDED' ); + return array( 'PAYMENT.CAPTURE.REFUNDED', 'PAYMENT.AUTHORIZATION.VOIDED' ); } /** diff --git a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php index 1f6a30e80..6b0b2ef14 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php @@ -46,6 +46,7 @@ class IdentityTokenTest extends TestCase public function testGenerateForCustomerReturnsToken() { + $id = 1; define( 'PPCP_FLAG_SUBSCRIPTION', true ); $token = Mockery::mock(Token::class); $token @@ -60,6 +61,7 @@ class IdentityTokenTest extends TestCase $this->settings->shouldReceive('has')->andReturn(true); $this->settings->shouldReceive('get')->andReturn(true); $this->customer_repository->shouldReceive('customer_id_for_user')->andReturn('prefix1'); + expect('update_user_meta')->with($id, 'ppcp_customer_id', 'prefix1'); $rawResponse = [ 'body' => '{"client_token":"abc123", "expires_in":3600}', @@ -97,6 +99,7 @@ class IdentityTokenTest extends TestCase public function testGenerateForCustomerFailsBecauseWpError() { + $id = 1; $token = Mockery::mock(Token::class); $token ->expects('token')->andReturn('bearer'); @@ -111,7 +114,8 @@ class IdentityTokenTest extends TestCase $this->logger->shouldReceive('debug'); $this->settings->shouldReceive('has')->andReturn(true); $this->settings->shouldReceive('get')->andReturn(true); - $this->customer_repository->shouldReceive('customer_id_for_user'); + $this->customer_repository->shouldReceive('customer_id_for_user')->andReturn('prefix1'); + expect('update_user_meta')->with($id, 'ppcp_customer_id', 'prefix1'); $this->expectException(RuntimeException::class); $this->sut->generate_for_user(1); @@ -119,6 +123,7 @@ class IdentityTokenTest extends TestCase public function testGenerateForCustomerFailsBecauseResponseCodeIsNot200() { + $id = 1; $token = Mockery::mock(Token::class); $token ->expects('token')->andReturn('bearer'); @@ -137,7 +142,8 @@ class IdentityTokenTest extends TestCase $this->logger->shouldReceive('debug'); $this->settings->shouldReceive('has')->andReturn(true); $this->settings->shouldReceive('get')->andReturn(true); - $this->customer_repository->shouldReceive('customer_id_for_user'); + $this->customer_repository->shouldReceive('customer_id_for_user')->andReturn('prefix1'); + expect('update_user_meta')->with($id, 'ppcp_customer_id', 'prefix1'); $this->expectException(PayPalApiException::class); $this->sut->generate_for_user(1); diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php index 642d9c4b6..4052febd2 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php @@ -1046,8 +1046,6 @@ class OrderEndpointTest extends TestCase $payer = Mockery::mock(Payer::class); $payer->expects('email_address')->andReturn('email@email.com'); - $payerName = Mockery::mock(PayerName::class); - $payer->expects('name')->andReturn($payerName); $payer->expects('to_array')->andReturn(['payer']); $result = $testee->create([$purchaseUnit], ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE, $payer); $this->assertEquals($expectedOrder, $result); @@ -1138,8 +1136,6 @@ class OrderEndpointTest extends TestCase $payer = Mockery::mock(Payer::class); $payer->expects('email_address')->andReturn('email@email.com'); - $payerName = Mockery::mock(PayerName::class); - $payer->expects('name')->andReturn($payerName); $payer->expects('to_array')->andReturn(['payer']); $testee->create([$purchaseUnit], ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, $payer); } @@ -1229,8 +1225,6 @@ class OrderEndpointTest extends TestCase $this->expectException(RuntimeException::class); $payer = Mockery::mock(Payer::class); $payer->expects('email_address')->andReturn('email@email.com'); - $payerName = Mockery::mock(PayerName::class); - $payer->expects('name')->andReturn($payerName); $payer->expects('to_array')->andReturn(['payer']); $testee->create([$purchaseUnit], ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE, $payer); } diff --git a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php index 00d863a9e..d185f37ea 100644 --- a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php @@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\TestCase; +use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; @@ -152,6 +153,7 @@ class CreateOrderEndpointTest extends TestCase $session_handler = Mockery::mock(SessionHandler::class); $settings = Mockery::mock(Settings::class); $early_order_handler = Mockery::mock(EarlyOrderHandler::class); + $settings->shouldReceive('has')->andReturnFalse(); $testee = new CreateOrderEndpoint( $request_data, @@ -163,6 +165,7 @@ class CreateOrderEndpointTest extends TestCase $settings, $early_order_handler, false, + CardBillingMode::MINIMAL_INPUT, new NullLogger() ); return array($payer_factory, $testee); diff --git a/tests/PHPUnit/Subscription/RenewalHandlerTest.php b/tests/PHPUnit/Subscription/RenewalHandlerTest.php index de3249fd6..86b725f49 100644 --- a/tests/PHPUnit/Subscription/RenewalHandlerTest.php +++ b/tests/PHPUnit/Subscription/RenewalHandlerTest.php @@ -23,6 +23,8 @@ use Mockery; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; class RenewalHandlerTest extends TestCase { @@ -48,6 +50,11 @@ class RenewalHandlerTest extends TestCase $this->shippingPreferenceFactory = Mockery::mock(ShippingPreferenceFactory::class); $this->payerFactory = Mockery::mock(PayerFactory::class); $this->environment = new Environment(new Dictionary([])); + $authorizedPaymentProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); + $settings = Mockery::mock(Settings::class); + $settings + ->shouldReceive('has') + ->andReturnFalse(); $this->logger->shouldReceive('error')->andReturnUsing(function ($msg) { throw new Exception($msg); @@ -61,7 +68,9 @@ class RenewalHandlerTest extends TestCase $this->purchaseUnitFactory, $this->shippingPreferenceFactory, $this->payerFactory, - $this->environment + $this->environment, + $settings, + $authorizedPaymentProcessor ); } diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index b9b2a87cb..ec901bafa 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -3,14 +3,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; -use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; -use Psr\Log\NullLogger; -use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; -use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -37,7 +30,6 @@ class WcGatewayTest extends TestCase private $settingsRenderer; private $funding_source_renderer; private $orderProcessor; - private $authorizedOrdersProcessor; private $settings; private $refundProcessor; private $onboardingState; @@ -45,10 +37,7 @@ class WcGatewayTest extends TestCase private $subscriptionHelper; private $environment; private $paymentTokenRepository; - private $shipping_preference_factory; private $logger; - private $paymentsEndpoint; - private $orderEndpoint; private $apiShopCountry; public function setUp(): void { @@ -60,7 +49,6 @@ class WcGatewayTest extends TestCase $this->settingsRenderer = Mockery::mock(SettingsRenderer::class); $this->orderProcessor = Mockery::mock(OrderProcessor::class); - $this->authorizedOrdersProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); $this->settings = Mockery::mock(Settings::class); $this->sessionHandler = Mockery::mock(SessionHandler::class); $this->refundProcessor = Mockery::mock(RefundProcessor::class); @@ -69,10 +57,7 @@ class WcGatewayTest extends TestCase $this->subscriptionHelper = Mockery::mock(SubscriptionHelper::class); $this->environment = Mockery::mock(Environment::class); $this->paymentTokenRepository = Mockery::mock(PaymentTokenRepository::class); - $this->shipping_preference_factory = Mockery::mock(ShippingPreferenceFactory::class); $this->logger = Mockery::mock(LoggerInterface::class); - $this->paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); - $this->orderEndpoint = Mockery::mock(OrderEndpoint::class); $this->funding_source_renderer = new FundingSourceRenderer($this->settings); $this->apiShopCountry = 'DE'; @@ -87,6 +72,7 @@ class WcGatewayTest extends TestCase $this->settings->shouldReceive('has')->andReturnFalse(); $this->logger->shouldReceive('info'); + $this->logger->shouldReceive('error'); } private function createGateway() @@ -95,7 +81,6 @@ class WcGatewayTest extends TestCase $this->settingsRenderer, $this->funding_source_renderer, $this->orderProcessor, - $this->authorizedOrdersProcessor, $this->settings, $this->sessionHandler, $this->refundProcessor, @@ -105,10 +90,7 @@ class WcGatewayTest extends TestCase PayPalGateway::ID, $this->environment, $this->paymentTokenRepository, - $this->shipping_preference_factory, $this->logger, - $this->paymentsEndpoint, - $this->orderEndpoint, $this->apiShopCountry ); } @@ -173,8 +155,10 @@ class WcGatewayTest extends TestCase when('wc_get_checkout_url') ->justReturn($redirectUrl); - expect('wc_add_notice') - ->with('Couldn\'t find order to process','error'); + $this->sessionHandler + ->shouldReceive('destroy_session_data'); + + expect('wc_add_notice'); $this->assertEquals( [ @@ -195,7 +179,6 @@ class WcGatewayTest extends TestCase ->andReturnFalse(); $this->orderProcessor ->expects('last_error') - ->twice() ->andReturn($lastError); $this->subscriptionHelper->shouldReceive('has_subscription')->with($orderId)->andReturn(true); $this->subscriptionHelper->shouldReceive('is_subscription_change_payment')->andReturn(true); @@ -206,6 +189,8 @@ class WcGatewayTest extends TestCase expect('wc_get_order') ->with($orderId) ->andReturn($wcOrder); + $this->sessionHandler + ->shouldReceive('destroy_session_data'); expect('wc_add_notice') ->with($lastError, 'error'); diff --git a/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php b/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php index a6a3ba7c2..10a99b422 100644 --- a/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php +++ b/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php @@ -27,6 +27,7 @@ class PayUponInvoiceHelperTest extends TestCase ['1942-02-31', false], ['01-01-1942', false], ['1942-01-01', true], + ['0001-01-01', false], ]; } diff --git a/tests/stubs/WC_Payment_Gateway.php b/tests/stubs/WC_Payment_Gateway.php index 6fbb823c8..bb87ee010 100644 --- a/tests/stubs/WC_Payment_Gateway.php +++ b/tests/stubs/WC_Payment_Gateway.php @@ -4,7 +4,7 @@ declare(strict_types=1); class WC_Payment_Gateway { - protected function get_option(string $key) : string { + public function get_option(string $key, $empty_value = null) { return $key; } @@ -19,4 +19,4 @@ class WC_Payment_Gateway public function process_admin_options() { } -} \ No newline at end of file +}