diff --git a/audit-ci.json b/audit-ci.json index 6a826db6b..77705e7b9 100644 --- a/audit-ci.json +++ b/audit-ci.json @@ -1,7 +1,9 @@ { "allowlist": [ "GHSA-wf5p-g6vw-rhxx", - "GHSA-rv95-896h-c2vc" + "GHSA-rv95-896h-c2vc", + "GHSA-grv7-fg5c-xmjg", + "GHSA-3h5v-q93c-6h6q" ], "moderate": true } diff --git a/public/index.html b/public/index.html index 6e2eba53c..1b8ff47f7 100755 --- a/public/index.html +++ b/public/index.html @@ -26,6 +26,5 @@ - diff --git a/src/payment/PageLoading.jsx b/src/payment/PageLoading.jsx index 15e55c167..de61fb2b7 100644 --- a/src/payment/PageLoading.jsx +++ b/src/payment/PageLoading.jsx @@ -1,7 +1,5 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { getConfig } from '@edx/frontend-platform'; -import { logInfo } from '@edx/frontend-platform/logging'; export default class PageLoading extends Component { renderSrMessage() { @@ -17,17 +15,6 @@ export default class PageLoading extends Component { } render() { - const { shouldRedirectToReceipt, orderNumber } = this.props; - - if (shouldRedirectToReceipt) { - logInfo(`Dynamic Payment Methods payment succeeded for edX order number ${orderNumber}, redirecting to ecommerce receipt page.`); - const queryParams = `order_number=${orderNumber}&disable_back_button=${Number(true)}&dpm_enabled=${true}`; - if (getConfig().ENVIRONMENT !== 'test') { - /* istanbul ignore next */ - global.location.assign(`${getConfig().ECOMMERCE_BASE_URL}/checkout/receipt/?${queryParams}`); - } - } - return (
{ + useEffect(() => { + const timer = setTimeout(() => { + logInfo(`Dynamic Payment Methods payment succeeded for edX order number ${orderNumber}, redirecting to ecommerce receipt page.`); + const queryParams = `order_number=${orderNumber}&disable_back_button=${Number(true)}&dpm_enabled=${true}`; + + if (getConfig().ENVIRONMENT !== 'test') { + /* istanbul ignore next */ + global.location.assign(`${getConfig().ECOMMERCE_BASE_URL}/checkout/receipt/?${queryParams}`); + } + }, 3000); // Delay the redirect to receipt page by 3 seconds to make sure ecomm order fulfillment is done. + + return () => clearTimeout(timer); // On unmount, clear the timer + }, [srMessage, orderNumber]); + + const renderSrMessage = () => { + if (!srMessage) { + return null; + } + + return ( + + {srMessage} + + ); + }; + + return ( +
+
+
+ {renderSrMessage()} +
+
+
+ ); +}; + +PageLoadingDynamicPaymentMethods.propTypes = { + srMessage: PropTypes.string.isRequired, + orderNumber: PropTypes.string, +}; + +PageLoadingDynamicPaymentMethods.defaultProps = { + orderNumber: null, +}; + +export default PageLoadingDynamicPaymentMethods; diff --git a/src/payment/PageLoadingDynamicPaymentMethods.test.jsx b/src/payment/PageLoadingDynamicPaymentMethods.test.jsx new file mode 100644 index 000000000..192fd2e75 --- /dev/null +++ b/src/payment/PageLoadingDynamicPaymentMethods.test.jsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; +import { render, act } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { logInfo } from '@edx/frontend-platform/logging'; + +import createRootReducer from '../data/reducers'; +import PageLoadingDynamicPaymentMethods from './PageLoadingDynamicPaymentMethods'; + +jest.mock('@edx/frontend-platform/logging', () => ({ + logInfo: jest.fn(), +})); + +describe('PageLoadingDynamicPaymentMethods', () => { + let store; + + beforeEach(() => { + store = createStore(createRootReducer()); + jest.useFakeTimers(); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + it('renders ', () => { + const component = ( + + + + + + ); + const { container: tree } = render(component); + expect(tree).toMatchSnapshot(); + }); + + it('it redirects to receipt page after 3 seconds delay', () => { + const orderNumber = 'EDX-100001'; + const logMessage = `Dynamic Payment Methods payment succeeded for edX order number ${orderNumber}, redirecting to ecommerce receipt page.`; + render( + + + + + , + ); + + act(() => { + jest.advanceTimersByTime(3000); + }); + expect(logInfo).toHaveBeenCalledWith(expect.stringMatching(logMessage)); + }); + + it('cleans up the timer on unmount', () => { + const { unmount } = render( + , + ); + unmount(); + act(() => { + jest.advanceTimersByTime(3000); + }); + expect(logInfo).not.toHaveBeenCalled(); + }); +}); diff --git a/src/payment/PaymentPage.jsx b/src/payment/PaymentPage.jsx index f0902f38e..bdf43cd4a 100644 --- a/src/payment/PaymentPage.jsx +++ b/src/payment/PaymentPage.jsx @@ -26,6 +26,7 @@ import EmptyCartMessage from './EmptyCartMessage'; import Cart from './cart/Cart'; import Checkout from './checkout/Checkout'; import { FormattedAlertList } from '../components/formatted-alert-list/FormattedAlertList'; +import PageLoadingDynamicPaymentMethods from './PageLoadingDynamicPaymentMethods'; class PaymentPage extends React.Component { constructor(props) { @@ -113,9 +114,8 @@ class PaymentPage extends React.Component { // lag between when the paymentStatus is no longer null but the redirect hasn't happened yet. if (shouldRedirectToReceipt) { return ( - ); diff --git a/src/payment/__snapshots__/PageLoadingDynamicPaymentMethods.test.jsx.snap b/src/payment/__snapshots__/PageLoadingDynamicPaymentMethods.test.jsx.snap new file mode 100644 index 000000000..c3dda9f75 --- /dev/null +++ b/src/payment/__snapshots__/PageLoadingDynamicPaymentMethods.test.jsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageLoadingDynamicPaymentMethods renders 1`] = ` +
+
+
+
+
+
+
+`; diff --git a/src/payment/checkout/Checkout.jsx b/src/payment/checkout/Checkout.jsx index 1972dc58d..04f63c32a 100644 --- a/src/payment/checkout/Checkout.jsx +++ b/src/payment/checkout/Checkout.jsx @@ -26,10 +26,28 @@ import { PayPalButton } from '../payment-methods/paypal'; import { ORDER_TYPES } from '../data/constants'; class Checkout extends React.Component { + constructor(props) { + super(props); + this.state = { + hasRedirectedToPaypal: false, + }; + } + componentDidMount() { this.props.fetchClientSecret(); } + handleRedirectToPaypal = () => { + const { loading, isBasketProcessing, isPaypalRedirect } = this.props; + const { hasRedirectedToPaypal } = this.state; + const submissionDisabled = loading || isBasketProcessing; + + if (!submissionDisabled && isPaypalRedirect && !hasRedirectedToPaypal) { + this.setState({ hasRedirectedToPaypal: true }); + this.handleSubmitPayPal(); + } + }; + handleSubmitPayPal = () => { // TO DO: after event parity, track data should be // sent only if the payment is processed, not on click @@ -161,6 +179,8 @@ class Checkout extends React.Component { const isBulkOrder = orderType === ORDER_TYPES.BULK_ENROLLMENT; const isQuantityUpdating = isBasketProcessing && loaded; + this.handleRedirectToPaypal(); + // Stripe element config // TODO: Move these to a better home const options = { @@ -314,6 +334,7 @@ Checkout.propTypes = { enableStripePaymentProcessor: PropTypes.bool, stripe: PropTypes.object, // eslint-disable-line react/forbid-prop-types clientSecretId: PropTypes.string, + isPaypalRedirect: PropTypes.bool, }; Checkout.defaultProps = { @@ -327,6 +348,7 @@ Checkout.defaultProps = { enableStripePaymentProcessor: false, stripe: null, clientSecretId: null, + isPaypalRedirect: false, }; const mapStateToProps = (state) => ({ diff --git a/src/payment/data/redux.test.js b/src/payment/data/redux.test.js index 356ed36d5..9cca8b3b7 100644 --- a/src/payment/data/redux.test.js +++ b/src/payment/data/redux.test.js @@ -114,6 +114,7 @@ describe('redux tests', () => { isEmpty: false, isPaymentRedirect: false, isRedirect: false, + isPaypalRedirect: false, }); }); @@ -135,6 +136,7 @@ describe('redux tests', () => { isEmpty: false, isPaymentRedirect: false, isRedirect: true, // this is also now true. + isPaypalRedirect: false, }); }); @@ -156,6 +158,7 @@ describe('redux tests', () => { isEmpty: false, isPaymentRedirect: true, // this is now true isRedirect: false, + isPaypalRedirect: false, }); }); }); diff --git a/src/payment/data/selectors.js b/src/payment/data/selectors.js index f4d329693..f75c46c0a 100644 --- a/src/payment/data/selectors.js +++ b/src/payment/data/selectors.js @@ -39,10 +39,13 @@ export const paymentSelector = createSelector( && queryParams.coupon_redeem_redirect == 1; // eslint-disable-line eqeqeq const isPaymentRedirect = !!queryParams && Boolean(queryParams.payment_intent); // Only klarna has redirect_status on URL + const isPaypalRedirect = !!queryParams + && queryParams.paypal_redirect == 1; // eslint-disable-line eqeqeq return { ...basket, isCouponRedeemRedirect, isPaymentRedirect, + isPaypalRedirect, isEmpty: basket.loaded && !basket.redirect && (!basket.products || basket.products.length === 0), isRedirect: