Skip to content

Commit

Permalink
Merge branch 'develop' into fix-qit-security
Browse files Browse the repository at this point in the history
  • Loading branch information
dkotter authored Jul 9, 2024
2 parents 60baec9 + 0149fa8 commit 61e6554
Show file tree
Hide file tree
Showing 77 changed files with 3,099 additions and 443 deletions.
15 changes: 15 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
*** Changelog ***
= 8.5.0 - 2024-xx-xx =
* Tweak - Additional visual improvement for the webhook configuration notice.
* Add - Allow changing display order of payment methods in the new checkout experience.
* Add - Update the payment method associated with a subscription to a PaymentMethod when it's using a Stripe Source that was migrated to PaymentMethods.
* Fix - Prevent subscriptions using Legacy SEPA from switching to Manual Renewal when disabling the Legacy experience.
* Tweak - Add a notice in checkout for Cash App transactions above 2000 USD to inform customers about the decline risk.
* Tweak - Improve the display of warning messages related to webhook configuration.
* Fix - When using a saved payment method, update the payment method's address immediately upon checkout. Fixes issues where Stripe may throw address validation errors.
* Tweak - Add a statement descriptor preview for Cash App Payments.
* Add - Allow customizing the title and description of the UPE payment methods.
* Fix - Ensure payments via redirect are processed through the webhook if the redirect never occurs. Resolves issues of orders being left as pending payment.
* Add - Introduce a way for store managers to automatically configure webhooks on their Stripe account with a single button in the admin settings.
* Fix - Ensure subscriptions purchased with iDEAL or Bancontact are correctly set to SEPA debit prior to processing the intitial payment.
* Tweak - Stripe API version updated to support 2024-06-20.
* Fix - Ensure SEPA tokens are attached to customers in the legacy checkout experience when the payment method is saved. This addresses subscription recurring payment "off-session" errors with SEPA.

= 8.4.0 - 2024-06-13 =
* Tweak - Resets the list of payment methods when any Stripe key is updated.
Expand Down
29 changes: 29 additions & 0 deletions client/blocks/upe/call-when-element-is-available.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Call a function when an element is available in the DOM.
*
* @param {string} selector The selector to look for.
* @param {Function} callable fThe function to call when the element is available.
* @param {Array} params The parameters to pass to the callable function.
*/
export function callWhenElementIsAvailable( selector, callable, params = [] ) {
const checkoutBlock = document.querySelector(
'[data-block-name="woocommerce/checkout"]'
);

if ( ! checkoutBlock ) {
return;
}

const observer = new MutationObserver( ( mutationList, obs ) => {
if ( document.querySelector( selector ) ) {
// Element found, run the function and disconnect the observer.
callable( ...params );
obs.disconnect();
}
} );

observer.observe( checkoutBlock, {
childList: true,
subtree: true,
} );
}
2 changes: 2 additions & 0 deletions client/blocks/upe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ Object.entries( getBlocksConfiguration()?.paymentMethodsConfig )
upeName,
upeMethods,
api,
upeConfig.description,
upeConfig.testingInstructions,
upeConfig.showSaveOption ?? false
),
edit: getDeferredIntentCreationUPEFields(
upeName,
upeMethods,
api,
upeConfig.description,
upeConfig.testingInstructions,
upeConfig.showSaveOption ?? false
),
Expand Down
23 changes: 2 additions & 21 deletions client/blocks/upe/token-label-updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* WC_Stripe_Payment_Tokens::get_token_label_overrides_for_checkout().
*/
import { getBlocksConfiguration } from 'wcstripe/blocks/utils';
import { callWhenElementIsAvailable } from 'wcstripe/blocks/upe/call-when-element-is-available';

/**
* Determines whether there are token label overrides.
Expand Down Expand Up @@ -57,26 +58,6 @@ export function updateTokenLabelsWhenLoaded() {
if ( hasTokenElements ) {
updateTokenLabels();
} else {
// Tokens are not loaded yet, set up an observer to trigger once they have been mounted.
const checkoutBlock = document.querySelector(
'[data-block-name="woocommerce/checkout"]'
);

if ( ! checkoutBlock ) {
return;
}

const observer = new MutationObserver( ( mutationList, obs ) => {
if ( document.querySelector( selector ) ) {
// Tokens found, run the function and disconnect the observer.
updateTokenLabels();
obs.disconnect();
}
} );

observer.observe( checkoutBlock, {
childList: true,
subtree: true,
} );
callWhenElementIsAvailable( selector, updateTokenLabels );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const PaymentElements = ( { api, ...props } ) => {
* @param {string} paymentMethodId
* @param {Array} upeMethods
* @param {WCStripeAPI} api
* @param {string} description
* @param {string} testingInstructions
* @param {boolean} showSaveOption
*
Expand All @@ -63,6 +64,7 @@ export const getDeferredIntentCreationUPEFields = (
paymentMethodId,
upeMethods,
api,
description,
testingInstructions,
showSaveOption
) => {
Expand All @@ -71,6 +73,7 @@ export const getDeferredIntentCreationUPEFields = (
paymentMethodId={ paymentMethodId }
upeMethods={ upeMethods }
api={ api }
description={ description }
testingInstructions={ testingInstructions }
showSaveOption={ showSaveOption }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
} from '../hooks';
import { getBlocksConfiguration } from 'wcstripe/blocks/utils';
import WCStripeAPI from 'wcstripe/api';
import {
maybeShowCashAppLimitNotice,
removeCashAppLimitNotice,
} from 'wcstripe/stripe-utils/cash-app-limit-notice-handler';

/**
* Gets the Stripe element options.
Expand Down Expand Up @@ -72,6 +76,7 @@ export function validateElements( elements ) {
* @param {*} args Additional arguments passed for payment processing on the Block Checkout.
* @param {WCStripeAPI} args.api The Stripe API object.
* @param {string} args.activePaymentMethod The currently selected/active payment method ID.
* @param {string} args.description The payment method description to display.
* @param {string} args.testingInstructions The testing instructions to display.
* @param {Object} args.eventRegistration The checkout event emitter registration object.
* @param {Object} args.emitResponse Various helpers for usage with observer response objects.
Expand All @@ -87,6 +92,7 @@ export function validateElements( elements ) {
const PaymentProcessor = ( {
api,
activePaymentMethod,
description,
testingInstructions,
eventRegistration: { onPaymentSetup, onCheckoutSuccess, onCheckoutFail },
emitResponse,
Expand All @@ -99,6 +105,10 @@ const PaymentProcessor = ( {
} ) => {
const stripe = useStripe();
const elements = useElements();
const [
selectedPaymentMethodType,
setSelectedPaymentMethodType,
] = useState( null );
const [ isPaymentElementComplete, setIsPaymentElementComplete ] = useState(
false
);
Expand Down Expand Up @@ -228,6 +238,19 @@ const PaymentProcessor = ( {
]
);

// Show the Cash App limit notice if the payment method is selected and the cart amount is higher than 2000 USD.
useEffect( () => {
if ( selectedPaymentMethodType === 'cashapp' ) {
maybeShowCashAppLimitNotice(
'.wc-block-checkout__payment-method .wc-block-components-notices',
Number( getBlocksConfiguration()?.cartTotal ),
true
);
} else {
removeCashAppLimitNotice();
}
}, [ selectedPaymentMethodType ] );

usePaymentCompleteHandler(
api,
stripe,
Expand All @@ -247,12 +270,19 @@ const PaymentProcessor = ( {

useStripeLink( api, elements, paymentMethodsConfig );

const updatePaymentElementCompletionStatus = ( event ) => {
setIsPaymentElementComplete( event.complete );
const onSelectedPaymentMethodChange = ( { value, complete } ) => {
setSelectedPaymentMethodType( value.type );
setIsPaymentElementComplete( complete );
};

return (
<>
<p
className="content"
dangerouslySetInnerHTML={ {
__html: description,
} }
/>
<p
className="content"
dangerouslySetInnerHTML={ {
Expand All @@ -261,7 +291,7 @@ const PaymentProcessor = ( {
/>
<PaymentElement
options={ getStripeElementOptions() }
onChange={ updatePaymentElementCompletionStatus }
onChange={ onSelectedPaymentMethodChange }
className="wcstripe-payment-element"
/>
</>
Expand Down
2 changes: 1 addition & 1 deletion client/classic/upe/deferred-intent.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ jQuery( function ( $ ) {
}
}

// On every page load and on hash change, check to see whether we should display the Voucher (Boleto/Oxxo) or Wallet (CashApp/WeChat Pay) modal.
// On every page load and on hash change, check to see whether we should display the Voucher (Boleto/Oxxo/Multibanco) or Wallet (CashApp/WeChat Pay) modal.
// Every page load is needed for the Pay for Order page which doesn't trigger the hash change.
maybeConfirmVoucherOrWalletPayment();
$( window ).on( 'hashchange', () => {
Expand Down
36 changes: 28 additions & 8 deletions client/classic/upe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { getStripeServerData, getUPETerms } from '../../stripe-utils';
import { legacyHashchangeHandler } from './legacy-support';
import './style.scss';
import './deferred-intent.js';
import {
maybeShowCashAppLimitNotice,
removeCashAppLimitNotice,
} from 'wcstripe/stripe-utils/cash-app-limit-notice-handler';

jQuery( function ( $ ) {
const key = getStripeServerData()?.key;
Expand Down Expand Up @@ -190,7 +194,11 @@ jQuery( function ( $ ) {
if ( error ) {
const upeType = formFields.wc_stripe_selected_upe_payment_type;

if ( upeType !== 'boleto' && upeType !== 'oxxo' ) {
if (
upeType !== 'boleto' &&
upeType !== 'oxxo' &&
upeType !== 'multibanco'
) {
await api.updateFailedOrder(
paymentIntentId,
response.order_id
Expand Down Expand Up @@ -274,14 +282,26 @@ jQuery( function ( $ ) {
}

// Handle the checkout form when WooCommerce Gateway Stripe is chosen.
$( 'form.checkout' ).on( 'checkout_place_order_stripe', function () {
if ( ! isUsingSavedPaymentMethod() ) {
if ( isUPEEnabled && paymentIntentId ) {
handleUPECheckout( $( this ) );
return false;
$( 'form.checkout' )
.on( 'checkout_place_order_stripe', function () {
if ( ! isUsingSavedPaymentMethod() ) {
if ( isUPEEnabled && paymentIntentId ) {
handleUPECheckout( $( this ) );
return false;
}
}
}
} );
} )
.on( 'change', 'input[name="payment_method"]', () => {
// Check to see whether we should display the Cash App limit notice.
if ( $( 'input#payment_method_stripe_cashapp' ).is( ':checked' ) ) {
maybeShowCashAppLimitNotice(
'.woocommerce-checkout-payment',
Number( getStripeServerData()?.cartTotal )
);
} else {
removeCashAppLimitNotice();
}
} );

// Add terms parameter to UPE if save payment information checkbox is checked.
// This shows required legal mandates when customer elects to save payment method during checkout.
Expand Down
6 changes: 5 additions & 1 deletion client/classic/upe/payment-processing.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export const createAndConfirmSetupIntent = (
};

/**
* Handles displaying the Boleto or Oxxo voucher to the customer and then redirecting
* Handles displaying the Boleto or Oxxo or Multibanco voucher to the customer and then redirecting
* them to the order received page once they close the voucher window.
*
* When processing a payment for one of our voucher payment methods on the checkout or order pay page,
Expand Down Expand Up @@ -361,6 +361,10 @@ export const confirmVoucherPayment = async ( api, jQueryForm ) => {
confirmPayment = await api
.getStripe()
.confirmBoletoPayment( clientSecret, {} );
} else if ( paymentMethodType === 'multibanco' ) {
confirmPayment = await api
.getStripe()
.confirmMultibancoPayment( clientSecret, {} );
} else {
confirmPayment = await api
.getStripe()
Expand Down
73 changes: 73 additions & 0 deletions client/components/webhook-description/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react';
import { WebhookDescription } from '..';
import { useAccount } from 'wcstripe/data/account';
import useWebhookStateMessage from 'wcstripe/settings/account-details/use-webhook-state-message';

jest.mock( 'wcstripe/data/account', () => ( {
useAccount: jest.fn(),
} ) );

jest.mock( 'wcstripe/settings/account-details/use-webhook-state-message' );

beforeEach( () => {
useAccount.mockReturnValue( {
data: { webhook_url: 'example.com' },
} );
} );

describe( 'WebhookDescription', () => {
it( 'regular message (not a warning), no information component', () => {
useWebhookStateMessage.mockImplementation( () => {
return {
message: 'Some message',
requestStatus: 'success',
refreshMessage: jest.fn(),
};
} );

render( <WebhookDescription isWebhookSecretEntered={ true } /> );

expect(
screen.queryByTestId( 'webhook-information' )
).not.toBeInTheDocument();
expect(
screen.queryByTestId( 'warning-icon' )
).not.toBeInTheDocument();
} );

it( 'regular message (not a warning), with information component', () => {
useWebhookStateMessage.mockImplementation( () => {
return {
message: 'Some message',
requestStatus: 'success',
refreshMessage: jest.fn(),
};
} );

render( <WebhookDescription isWebhookSecretEntered={ false } /> );

expect(
screen.queryByTestId( 'webhook-information' )
).toBeInTheDocument();
expect(
screen.queryByTestId( 'warning-icon' )
).not.toBeInTheDocument();
} );

it( 'warning message, with information component', () => {
useWebhookStateMessage.mockImplementation( () => {
return {
message: 'Warning: Some message',
requestStatus: 'success',
refreshMessage: jest.fn(),
};
} );

render( <WebhookDescription isWebhookSecretEntered={ false } /> );

expect(
screen.queryByTestId( 'webhook-information' )
).toBeInTheDocument();
expect( screen.queryByTestId( 'warning-icon' ) ).toBeInTheDocument();
} );
} );
Loading

0 comments on commit 61e6554

Please sign in to comment.