diff --git a/audit-ci.json b/audit-ci.json index b6b51da30..d598dccb1 100644 --- a/audit-ci.json +++ b/audit-ci.json @@ -1,10 +1,10 @@ { "allowlist": [ - "GHSA-hpx4-r86g-5jrg", "GHSA-wf5p-g6vw-rhxx", "GHSA-cxjh-pqwp-8mfp", "GHSA-wr3j-pwj9-hqq6", - "GHSA-rv95-896h-c2vc" + "GHSA-rv95-896h-c2vc", + "GHSA-8cp3-66vr-3r4c" ], "moderate": true } diff --git a/docs/how_tos/feedback.rst b/docs/how_tos/feedback.rst index 5e0b41d32..7a73c4446 100644 --- a/docs/how_tos/feedback.rst +++ b/docs/how_tos/feedback.rst @@ -374,6 +374,7 @@ pre-built APIs that do not follow the format of the feedback module in the: * `feedback/data/sagas.js`_ * ``basket-changed-error-message`` + * ``dynamic-payment-methods-country-not-compatible`` * ``transaction-declined-message`` * ``error_message`` in URL parameters diff --git a/src/payment/PaymentPage.jsx b/src/payment/PaymentPage.jsx index 9bdda4042..801f5e517 100644 --- a/src/payment/PaymentPage.jsx +++ b/src/payment/PaymentPage.jsx @@ -14,7 +14,7 @@ import { AppContext } from '@edx/frontend-platform/react'; import { sendPageEvent } from '@edx/frontend-platform/analytics'; import messages from './PaymentPage.messages'; -import { handleApiError } from './data/handleRequestError'; +import handleRequestError from './data/handleRequestError'; // Actions import { fetchBasket } from './data/actions'; @@ -80,13 +80,18 @@ class PaymentPage extends React.Component { // Get Payment Intent to retrieve the payment status and order number associated with this DPM payment. // If this is not a Stripe dynamic payment methods (BNPL), URL will not contain any params // and should not retrieve the Payment Intent. + // TODO: depending on if we'll use this MFE in the future, refactor to follow the redux pattern with actions + // and reducers for getting the Payment Intent is more appropriate. const searchParams = new URLSearchParams(global.location.search); const clientSecretId = searchParams.get('payment_intent_client_secret'); if (clientSecretId) { - const { paymentIntent, error } = await this.state.stripe.retrievePaymentIntent(clientSecretId); - if (error) { handleApiError(error); } - this.setState({ orderNumber: paymentIntent.description }); - this.setState({ paymentStatus: paymentIntent.status }); + try { + const { paymentIntent } = await this.state.stripe.retrievePaymentIntent(clientSecretId); + this.setState({ orderNumber: paymentIntent?.description }); + this.setState({ paymentStatus: paymentIntent?.status }); + } catch (error) { + handleRequestError(error); + } } }; @@ -118,7 +123,7 @@ class PaymentPage extends React.Component { } // If this is a redirect from Stripe Dynamic Payment Methods, show loading icon until getPaymentStatus is done. - if (isPaymentRedirect && (paymentStatus !== 'requires_payment_method' || paymentStatus !== 'canceled')) { + if (isPaymentRedirect && paymentStatus === null) { return ( 0 || stripeElementErrors) { diff --git a/src/payment/checkout/payment-form/StripePaymentForm.test.jsx b/src/payment/checkout/payment-form/StripePaymentForm.test.jsx index 2a43c9a8e..be7424aa1 100644 --- a/src/payment/checkout/payment-form/StripePaymentForm.test.jsx +++ b/src/payment/checkout/payment-form/StripePaymentForm.test.jsx @@ -23,7 +23,7 @@ jest.mock('@edx/frontend-platform/analytics', () => ({ jest.useFakeTimers('modern'); const validateRequiredFieldsMock = jest.spyOn(formValidators, 'validateRequiredFields'); - +const validateCountryPaymentMethodCompatibilityMock = jest.spyOn(formValidators, 'validateCountryPaymentMethodCompatibility'); const mockStore = configureMockStore(); configureI18n({ @@ -260,8 +260,10 @@ describe('', () => { const testData = [ [ { firstName: 'This field is required' }, + { country: 'Country not available with selected payment method' }, new SubmissionError({ firstName: 'This field is required', + country: 'Country not available with selected payment method', }), ], [ @@ -272,6 +274,7 @@ describe('', () => { testData.forEach((testCaseData) => { validateRequiredFieldsMock.mockReturnValueOnce(testCaseData[0]); + validateCountryPaymentMethodCompatibilityMock.mockReturnValueOnce(testCaseData[0]); if (testCaseData[1]) { expect(() => fireEvent.click(screen.getByText('Place Order'))); expect(submitStripePayment).not.toHaveBeenCalled(); @@ -294,4 +297,22 @@ describe('', () => { expect(formValidators.validateRequiredFields(values)).toEqual(expectedErrors); }); }); + + describe('validateCountryPaymentMethodCompatibility', () => { + it('returns errors if country is not compatible with Dynamic Payment Method (BNPL Affirm)', () => { + const values = { + country: 'BR', + }; + const expectedErrors = { + country: 'payment.form.errors.dynamic_payment_methods_not_compatible.country', + }; + const isDynamicPaymentMethodsEnabled = true; + const stripeSelectedPaymentMethod = 'affirm'; + expect(formValidators.validateCountryPaymentMethodCompatibility( + isDynamicPaymentMethodsEnabled, + stripeSelectedPaymentMethod, + values.country, + )).toEqual(expectedErrors); + }); + }); }); diff --git a/src/payment/checkout/payment-form/utils/form-validators.js b/src/payment/checkout/payment-form/utils/form-validators.js index 25a04ca39..f68246039 100644 --- a/src/payment/checkout/payment-form/utils/form-validators.js +++ b/src/payment/checkout/payment-form/utils/form-validators.js @@ -126,6 +126,26 @@ export function validateRequiredFields(values) { return errors; } +export function validateCountryPaymentMethodCompatibility( + isDynamicPaymentMethodsEnabled, + stripeSelectedPaymentMethod, + selectedCountry, +) { + const errors = {}; + + // Only adding country validation on the form level for BNPL Affirm. + // For Klarna, there is validation on the Stripe API level, + // which is handled with error code 'dynamic-payment-methods-country-not-compatible' + if (isDynamicPaymentMethodsEnabled && stripeSelectedPaymentMethod === 'affirm') { + const countryListCompatibleAffirm = ['CA', 'US']; + if (!countryListCompatibleAffirm.includes(selectedCountry)) { + errors.country = 'payment.form.errors.dynamic_payment_methods_not_compatible.country'; + } + } + + return errors; +} + export function validateCardDetails(cardExpirationMonth, cardExpirationYear) { const errors = {};