+ { __(
+ 'Are you sure you want to enable test mode?',
+ 'woocommerce-payments'
+ ) }
+
+
+ { __(
+ "Test mode lets you try out payments, refunds, disputes and other such processes as you're working on your store " +
+ 'without handling live payment information. ' +
+ 'All incoming orders will be simulated, and test mode will have to be disabled before you can accept real orders.',
+ 'woocommerce-payments'
+ ) }
+
+
+ { __( 'Learn more about test mode', 'woocommerce-payments' ) }
+
+
+ );
+};
+
+export default TestModeConfirmationModal;
diff --git a/client/settings/general-settings/test/general-settings.test.js b/client/settings/general-settings/test/general-settings.test.js
index 93f27d56b2a..2e9ae53fcf6 100644
--- a/client/settings/general-settings/test/general-settings.test.js
+++ b/client/settings/general-settings/test/general-settings.test.js
@@ -71,4 +71,43 @@ describe( 'GeneralSettings', () => {
);
}
);
+
+ it.each( [ [ true ], [ false ] ] )(
+ 'display of CheckBox when initial Test Mode = %s',
+ ( isEnabled ) => {
+ useTestMode.mockReturnValue( [ isEnabled, jest.fn() ] );
+ render( );
+ const enableTestModeCheckbox = screen.getByLabelText(
+ 'Enable test mode'
+ );
+
+ let expectation = expect( enableTestModeCheckbox );
+ if ( ! isEnabled ) {
+ expectation = expectation.not;
+ }
+ expectation.toBeChecked();
+ }
+ );
+
+ it.each( [ [ true ], [ false ] ] )(
+ 'Checks Confirmation Modal display when initial Test Mode = %s',
+ ( isEnabled ) => {
+ useTestMode.mockReturnValue( [ isEnabled, jest.fn() ] );
+ render( );
+ const enableTestModeCheckbox = screen.getByLabelText(
+ 'Enable test mode'
+ );
+ fireEvent.click( enableTestModeCheckbox );
+
+ let expectation = expect(
+ screen.queryByText(
+ 'Are you sure you want to enable test mode?'
+ )
+ );
+ if ( isEnabled ) {
+ expectation = expectation.not;
+ }
+ expectation.toBeInTheDocument();
+ }
+ );
} );
diff --git a/client/settings/general-settings/test/test-mode-confirm-modal.test.js b/client/settings/general-settings/test/test-mode-confirm-modal.test.js
new file mode 100644
index 00000000000..df247142d81
--- /dev/null
+++ b/client/settings/general-settings/test/test-mode-confirm-modal.test.js
@@ -0,0 +1,50 @@
+/** @format */
+
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import user from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import TestModeConfirmationModal from '../test-mode-confirm-modal';
+
+const mockOnClose = jest.fn();
+const mockOnConfirm = jest.fn();
+
+describe( 'Dev Mode Confirmation Modal', () => {
+ const renderTestModeConfirmationModal = () => {
+ return render(
+
+ );
+ };
+
+ it( 'Dev mode confirmation modal asks confirmation', () => {
+ renderTestModeConfirmationModal();
+ expect(
+ screen.queryByText( 'Are you sure you want to enable test mode?' )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'triggers the onClose function on close button click', () => {
+ renderTestModeConfirmationModal();
+ const closeButton = screen.queryByRole( 'button', { name: 'Cancel' } );
+ expect( mockOnClose ).not.toBeCalled();
+ user.click( closeButton );
+ expect( mockOnClose ).toBeCalled();
+ } );
+
+ it( 'triggers the onConfirm function on Enable button click', () => {
+ renderTestModeConfirmationModal();
+ const enableButton = screen.queryByRole( 'button', { name: 'Enable' } );
+ expect( mockOnConfirm ).not.toBeCalled();
+ user.click( enableButton );
+ expect( mockOnConfirm ).toBeCalled();
+ } );
+} );
diff --git a/client/settings/support-phone-input/index.js b/client/settings/support-phone-input/index.js
index 28a3015e747..4a9259b62d4 100644
--- a/client/settings/support-phone-input/index.js
+++ b/client/settings/support-phone-input/index.js
@@ -25,9 +25,7 @@ const SupportPhoneInput = ( { setInputVallid } ) => {
const isEmptyPhoneValid = supportPhone === '' && currentPhone === '';
const isDevModeEnabled = useDevMode();
const isTestPhoneValid =
- isDevModeEnabled &&
- ( supportPhone === '+1000-000-0000' ||
- supportPhone === '+10000000000' );
+ isDevModeEnabled && supportPhone === '+10000000000';
const [ isPhoneValid, setPhoneValidity ] = useState( true );
if ( ! isTestPhoneValid && ! isPhoneValid && ! isEmptyPhoneValid ) {
@@ -53,7 +51,7 @@ const SupportPhoneInput = ( { setInputVallid } ) => {
let labelText = __( 'Support phone number', 'woocommerce-payments' );
if ( isDevModeEnabled ) {
labelText += __(
- ' (+1 000-000-0000 can be used in dev mode)',
+ ' (+1 0000000000 can be used in dev mode)',
'woocommerce-payments'
);
}
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index 02e41836255..678afa250a0 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -725,15 +725,6 @@ public function should_use_stripe_platform_on_checkout_page() {
return false;
}
- /**
- * Renders the credit card input fields needed to get the user's payment information on the checkout page.
- *
- * We also add the JavaScript which drives the UI.
- */
- public function payment_fields() {
- do_action( 'wc_payments_add_payment_fields' );
- }
-
/**
* Checks whether the new payment process should be used to pay for a given order.
*
diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php
index 18198308323..9851a2ea0c2 100644
--- a/includes/class-wc-payments-checkout.php
+++ b/includes/class-wc-payments-checkout.php
@@ -7,18 +7,21 @@
namespace WCPay;
+use Exception;
use Jetpack_Options;
use WC_AJAX;
use WC_Checkout;
-use WC_Payment_Gateway_WCPay;
use WC_Payments;
use WC_Payments_Account;
use WC_Payments_Customer_Service;
-use WC_Payments_Features;
use WC_Payments_Fraud_Service;
use WC_Payments_Utils;
+use WC_Payments_Features;
+use WCPay\Constants\Payment_Method;
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
+use WCPay\Payment_Methods\UPE_Payment_Gateway;
use WCPay\WooPay\WooPay_Utilities;
+use WCPay\Payment_Methods\UPE_Payment_Method;
/**
@@ -29,7 +32,7 @@ class WC_Payments_Checkout {
/**
* WC Payments Gateway.
*
- * @var WC_Payment_Gateway_WCPay
+ * @var UPE_Payment_Gateway
*/
protected $gateway;
@@ -64,14 +67,14 @@ class WC_Payments_Checkout {
/**
* Construct.
*
- * @param WC_Payment_Gateway_WCPay $gateway WC Payment Gateway.
- * @param WooPay_Utilities $woopay_util WooPay Utilities.
- * @param WC_Payments_Account $account WC Payments Account.
- * @param WC_Payments_Customer_Service $customer_service WC Payments Customer Service.
- * @param WC_Payments_Fraud_Service $fraud_service Fraud service instance.
+ * @param UPE_Payment_Gateway $gateway WC Payment Gateway.
+ * @param WooPay_Utilities $woopay_util WooPay Utilities.
+ * @param WC_Payments_Account $account WC Payments Account.
+ * @param WC_Payments_Customer_Service $customer_service WC Payments Customer Service.
+ * @param WC_Payments_Fraud_Service $fraud_service Fraud service instance.
*/
public function __construct(
- WC_Payment_Gateway_WCPay $gateway,
+ UPE_Payment_Gateway $gateway,
WooPay_Utilities $woopay_util,
WC_Payments_Account $account,
WC_Payments_Customer_Service $customer_service,
@@ -90,16 +93,51 @@ public function __construct(
* @return void
*/
public function init_hooks() {
- add_action( 'wc_payments_add_payment_fields', [ $this, 'payment_fields' ] );
+ add_action( 'wc_payments_set_gateway', [ $this, 'set_gateway' ] );
+ add_action( 'wc_payments_add_upe_payment_fields', [ $this, 'payment_fields' ] );
+ add_action( 'woocommerce_after_account_payment_methods', [ $this->gateway, 'remove_upe_setup_intent_from_session' ], 10, 0 );
+ add_action( 'woocommerce_subscription_payment_method_updated', [ $this->gateway, 'remove_upe_setup_intent_from_session' ], 10, 0 );
+ add_action( 'woocommerce_order_payment_status_changed', [ get_class( $this->gateway ), 'remove_upe_payment_intent_from_session' ], 10, 0 );
+ add_action( 'wp', [ $this->gateway, 'maybe_process_upe_redirect' ] );
+ add_action( 'wc_ajax_wcpay_log_payment_error', [ $this->gateway, 'log_payment_error_ajax' ] );
+ add_action( 'wp_ajax_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] );
+ add_action( 'wp_ajax_nopriv_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] );
+ add_action( 'switch_theme', [ $this->gateway, 'clear_upe_appearance_transient' ] );
+ add_action( 'woocommerce_woocommerce_payments_updated', [ $this->gateway, 'clear_upe_appearance_transient' ] );
+ add_action( 'wc_ajax_wcpay_init_setup_intent', [ $this->gateway, 'init_setup_intent_ajax' ] );
+ add_action( 'wc_ajax_wcpay_log_payment_error', [ $this->gateway, 'log_payment_error_ajax' ] );
+
+ add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 );
}
/**
- * Enqueues and localizes WCPay's checkout scripts.
+ * Registers all scripts, necessary for the gateway.
*/
- public function enqueue_payment_scripts() {
- wp_localize_script( 'WCPAY_CHECKOUT', 'wcpayConfig', WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() );
- wp_enqueue_script( 'WCPAY_CHECKOUT' );
+ public function register_scripts() {
+ // Register Stripe's JavaScript using the same ID as the Stripe Gateway plugin. This prevents this JS being
+ // loaded twice in the event a site has both plugins enabled. We still run the risk of different plugins
+ // loading different versions however. If Stripe release a v4 of their JavaScript, we could consider
+ // changing the ID to stripe_v4. This would allow older plugins to keep using v3 while we used any new
+ // feature in v4. Stripe have allowed loading of 2 different versions of stripe.js in the past (
+ // https://stripe.com/docs/stripe-js/elements/migrating).
+ wp_register_script(
+ 'stripe',
+ 'https://js.stripe.com/v3/',
+ [],
+ '3.0',
+ true
+ );
+
+ $script_dependencies = [ 'stripe', 'wc-checkout', 'wp-i18n' ];
+
+ if ( $this->gateway->supports( 'tokenization' ) ) {
+ $script_dependencies[] = 'woocommerce-tokenization-form';
+ }
+
+ $script = 'dist/checkout';
+
+ WC_Payments::register_script_with_dependencies( 'wcpay-upe-checkout', $script, $script_dependencies );
}
/**
@@ -132,7 +170,6 @@ public function register_scripts_for_zero_order_total() {
* @return array
*/
public function get_payment_fields_js_config() {
-
// Needed to init the hooks.
WC_Checkout::instance();
@@ -179,11 +216,140 @@ public function get_payment_fields_js_config() {
*
* @param array $js_config The JS config for the payment fields.
*/
- return apply_filters( 'wcpay_payment_fields_js_config', $js_config );
+ $payment_fields = apply_filters( 'wcpay_payment_fields_js_config', $js_config );
+
+ $payment_fields['accountDescriptor'] = $this->gateway->get_account_statement_descriptor();
+ $payment_fields['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' );
+ $payment_fields['gatewayId'] = UPE_Payment_Gateway::GATEWAY_ID;
+ $payment_fields['isCheckout'] = is_checkout();
+ $payment_fields['paymentMethodsConfig'] = $this->get_enabled_payment_method_config();
+ $payment_fields['testMode'] = WC_Payments::mode()->is_test();
+ $payment_fields['upeAppearance'] = get_transient( UPE_Payment_Gateway::UPE_APPEARANCE_TRANSIENT );
+ $payment_fields['wcBlocksUPEAppearance'] = get_transient( UPE_Payment_Gateway::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
+ $payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart();
+ $payment_fields['currency'] = get_woocommerce_currency();
+ $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 );
+ $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() );
+
+ $enabled_billing_fields = [];
+ foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) {
+ if ( ! isset( $billing_field_options['enabled'] ) || $billing_field_options['enabled'] ) {
+ $enabled_billing_fields[] = $billing_field;
+ }
+ }
+ $payment_fields['enabledBillingFields'] = $enabled_billing_fields;
+
+ if ( is_wc_endpoint_url( 'order-pay' ) ) {
+ if ( $this->gateway->is_subscriptions_enabled() && $this->gateway->is_changing_payment_method_for_subscription() ) {
+ $payment_fields['isChangingPayment'] = true;
+ $payment_fields['addPaymentReturnURL'] = esc_url_raw( home_url( add_query_arg( [] ) ) );
+
+ if ( $this->gateway->is_setup_intent_success_creation_redirection() && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ) ) ) {
+ $setup_intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
+ $token = $this->gateway->create_token_from_setup_intent( $setup_intent_id, wp_get_current_user() );
+ if ( null !== $token ) {
+ $payment_fields['newTokenFormId'] = '#wc-' . $token->get_gateway_id() . '-payment-token-' . $token->get_id();
+ }
+ }
+ return $payment_fields; // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in.
+ }
+
+ $payment_fields['isOrderPay'] = true;
+ $order_id = absint( get_query_var( 'order-pay' ) );
+ $payment_fields['orderId'] = $order_id;
+ $order = wc_get_order( $order_id );
+
+ if ( is_a( $order, 'WC_Order' ) ) {
+ $order_currency = $order->get_currency();
+ $payment_fields['currency'] = $order_currency;
+ $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $order->get_total(), $order_currency );
+ $payment_fields['orderReturnURL'] = esc_url_raw(
+ add_query_arg(
+ [
+ 'wc_payment_method' => UPE_Payment_Gateway::GATEWAY_ID,
+ '_wpnonce' => wp_create_nonce( 'wcpay_process_redirect_order_nonce' ),
+ ],
+ $this->gateway->get_return_url( $order )
+ )
+ );
+ }
+ }
+
+ /**
+ * Allows filtering for the payment fields.
+ *
+ * @param array $payment_fields The payment fields.
+ */
+ return apply_filters( 'wcpay_payment_fields_js_config', $payment_fields ); // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in.
}
/**
- * Renders the credit card input fields needed to get the user's payment information on the checkout page.
+ * Checks if WooPay is enabled.
+ *
+ * @return bool - True if WooPay enabled, false otherwise.
+ */
+ private function is_woopay_enabled() {
+ return WC_Payments_Features::is_woopay_eligible() && 'yes' === $this->gateway->get_option( 'platform_checkout', 'no' ) && WC_Payments_Features::is_woopay_express_checkout_enabled();
+ }
+
+ /**
+ * Gets payment method settings to pass to client scripts
+ *
+ * @return array
+ */
+ public function get_enabled_payment_method_config() {
+ $settings = [];
+ $enabled_payment_methods = $this->gateway->get_payment_method_ids_enabled_at_checkout();
+
+ foreach ( $enabled_payment_methods as $payment_method_id ) {
+ // Link by Stripe should be validated with available fees.
+ if ( Payment_Method::LINK === $payment_method_id ) {
+ if ( ! in_array( Payment_Method::LINK, array_keys( $this->account->get_fees() ), true ) ) {
+ continue;
+ }
+ }
+
+ $payment_method = $this->gateway->wc_payments_get_payment_method_by_id( $payment_method_id );
+ $settings[ $payment_method_id ] = [
+ 'isReusable' => $payment_method->is_reusable(),
+ 'title' => $payment_method->get_title(),
+ 'icon' => $payment_method->get_icon(),
+ 'showSaveOption' => $this->should_upe_payment_method_show_save_option( $payment_method ),
+ 'countries' => $payment_method->get_countries(),
+ ];
+
+ $gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id );
+ $settings[ $payment_method_id ]['upePaymentIntentData'] = $this->gateway->get_payment_intent_data_from_session( $payment_method_id );
+ $settings[ $payment_method_id ]['upeSetupIntentData'] = $this->gateway->get_setup_intent_data_from_session( $payment_method_id );
+ $settings[ $payment_method_id ]['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html(
+ /* translators: link to Stripe testing page */
+ $payment_method->get_testing_instructions(),
+ [
+ 'strong' => '',
+ 'a' => '',
+ ]
+ );
+ $settings[ $payment_method_id ]['forceNetworkSavedCards'] = $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page();
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Checks if the save option for a payment method should be displayed or not.
+ *
+ * @param UPE_Payment_Method $payment_method UPE Payment Method instance.
+ * @return bool - True if the payment method is reusable and the saved cards feature is enabled for the gateway and there is no subscription item in the cart, false otherwise.
+ */
+ private function should_upe_payment_method_show_save_option( $payment_method ) {
+ if ( $payment_method->is_reusable() ) {
+ return $this->gateway->is_saved_cards_enabled() && ! $this->gateway->is_subscription_item_in_cart();
+ }
+ return false;
+ }
+
+ /**
+ * Renders the UPE input fields needed to get the user's payment information on the checkout page.
*
* We also add the JavaScript which drives the UI.
*/
@@ -191,15 +357,28 @@ public function payment_fields() {
try {
$display_tokenization = $this->gateway->supports( 'tokenization' ) && ( is_checkout() || is_add_payment_method_page() );
- add_action( 'wp_footer', [ $this, 'enqueue_payment_scripts' ] );
+ /**
+ * Localizing scripts within shortcodes does not work in WP 5.9,
+ * but we need `$this->get_payment_fields_js_config` to be called
+ * before `$this->saved_payment_methods()`.
+ */
+ $payment_fields = $this->get_payment_fields_js_config();
+ $upe_object_name = 'wcpay_upe_config';
+ wp_enqueue_script( 'wcpay-upe-checkout' );
+ add_action(
+ 'wp_footer',
+ function() use ( $payment_fields, $upe_object_name ) {
+ wp_localize_script( 'wcpay-upe-checkout', $upe_object_name, $payment_fields );
+ }
+ );
$prepared_customer_data = $this->customer_service->get_prepared_customer_data();
if ( ! empty( $prepared_customer_data ) ) {
- wp_localize_script( 'WCPAY_CHECKOUT', 'wcpayCustomerData', $prepared_customer_data );
+ wp_localize_script( 'wcpay-upe-checkout', 'wcpayCustomerData', $prepared_customer_data );
}
WC_Payments_Utils::enqueue_style(
- 'WCPAY_CHECKOUT',
+ 'wcpay-upe-checkout',
plugins_url( 'dist/checkout.css', WCPAY_PLUGIN_FILE ),
[],
WC_Payments::get_file_version( 'dist/checkout.css' ),
@@ -215,14 +394,17 @@ public function payment_fields() {
is_test() ) : ?>