From f462afdd1fec2d8dd5c267caba93bbcbbddbb72f Mon Sep 17 00:00:00 2001 From: Juliana Kang Date: Thu, 16 May 2024 13:00:12 -0400 Subject: [PATCH 1/9] feat: Added paypal redirect (#11) REV-4049 --- src/payment/PageLoading.jsx | 20 ----- .../PageLoadingDynamicPaymentMethods.jsx | 58 ++++++++++++++ .../PageLoadingDynamicPaymentMethods.test.jsx | 77 +++++++++++++++++++ src/payment/PaymentPage.jsx | 4 +- ...LoadingDynamicPaymentMethods.test.jsx.snap | 18 +++++ 5 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 src/payment/PageLoadingDynamicPaymentMethods.jsx create mode 100644 src/payment/PageLoadingDynamicPaymentMethods.test.jsx create mode 100644 src/payment/__snapshots__/PageLoadingDynamicPaymentMethods.test.jsx.snap 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`] = ` +
+
+
+
+
+
+
+`; From d3614a9ee00254b9a3f3bf83c5e99efe0b2c1406 Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Tue, 28 May 2024 14:36:43 -0400 Subject: [PATCH 2/9] feat: remove cybersource script (#7) --- public/index.html | 1 - 1 file changed, 1 deletion(-) 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 @@
- From e0f3020970ff4177770f01fd35f32fba7d1cf434 Mon Sep 17 00:00:00 2001 From: Batanayi Matuku Date: Fri, 5 Jul 2024 09:42:14 +0000 Subject: [PATCH 3/9] fix: Added paypal redirect feature triggered buy the frontastic checkout --- src/payment/checkout/Checkout.jsx | 8 ++++++++ src/payment/data/selectors.js | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/payment/checkout/Checkout.jsx b/src/payment/checkout/Checkout.jsx index 1972dc58d..9dd9c8561 100644 --- a/src/payment/checkout/Checkout.jsx +++ b/src/payment/checkout/Checkout.jsx @@ -156,11 +156,17 @@ class Checkout extends React.Component { submitting, orderType, stripe, + isPaypalRedirect, } = this.props; const submissionDisabled = loading || isBasketProcessing; const isBulkOrder = orderType === ORDER_TYPES.BULK_ENROLLMENT; const isQuantityUpdating = isBasketProcessing && loaded; + if (!submissionDisabled && isPaypalRedirect) { + // auto submit to paypal since the paypal redirect flag is set in the incoming rquiest + this.handleSubmitPayPal(); + } + // Stripe element config // TODO: Move these to a better home const options = { @@ -314,6 +320,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 +334,7 @@ Checkout.defaultProps = { enableStripePaymentProcessor: false, stripe: null, clientSecretId: null, + isPaypalRedirect: false, }; const mapStateToProps = (state) => ({ 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: From 9e48d763806ff369ffbcf2abfedd5d5e0f0e1954 Mon Sep 17 00:00:00 2001 From: Batanayi Matuku Date: Mon, 8 Jul 2024 20:10:26 +0200 Subject: [PATCH 4/9] fix: commit message --- audit-ci.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 } From 1073c0bb2c7a4750aca37a9d2f72df386ec5b32c Mon Sep 17 00:00:00 2001 From: Batanayi Matuku Date: Mon, 8 Jul 2024 19:55:48 +0200 Subject: [PATCH 5/9] fix: resolved new function referencing --- src/payment/checkout/Checkout.jsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/payment/checkout/Checkout.jsx b/src/payment/checkout/Checkout.jsx index 9dd9c8561..e06604b9a 100644 --- a/src/payment/checkout/Checkout.jsx +++ b/src/payment/checkout/Checkout.jsx @@ -28,8 +28,19 @@ import { ORDER_TYPES } from '../data/constants'; class Checkout extends React.Component { componentDidMount() { this.props.fetchClientSecret(); + this.handleRedirectToPaypal(); } + handleRedirectToPaypal = () => { + const { loading, isBasketProcessing, isPaypalRedirect } = this.props; + const submissionDisabled = loading || isBasketProcessing; + + if (!submissionDisabled && isPaypalRedirect) { + // auto submit to paypal since the paypal redirect flag is set in the incoming request + this.handleSubmitPayPal(); + } + }; + handleSubmitPayPal = () => { // TO DO: after event parity, track data should be // sent only if the payment is processed, not on click @@ -156,17 +167,11 @@ class Checkout extends React.Component { submitting, orderType, stripe, - isPaypalRedirect, } = this.props; const submissionDisabled = loading || isBasketProcessing; const isBulkOrder = orderType === ORDER_TYPES.BULK_ENROLLMENT; const isQuantityUpdating = isBasketProcessing && loaded; - if (!submissionDisabled && isPaypalRedirect) { - // auto submit to paypal since the paypal redirect flag is set in the incoming rquiest - this.handleSubmitPayPal(); - } - // Stripe element config // TODO: Move these to a better home const options = { From b5370e7a905519c0ab01e02910f3a362b785bd41 Mon Sep 17 00:00:00 2001 From: bdizha Date: Fri, 5 Jul 2024 12:49:52 +0000 Subject: [PATCH 6/9] fix: Updated a typo --- src/payment/checkout/Checkout.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/payment/checkout/Checkout.jsx b/src/payment/checkout/Checkout.jsx index e06604b9a..34fd45ef0 100644 --- a/src/payment/checkout/Checkout.jsx +++ b/src/payment/checkout/Checkout.jsx @@ -172,6 +172,11 @@ class Checkout extends React.Component { const isBulkOrder = orderType === ORDER_TYPES.BULK_ENROLLMENT; const isQuantityUpdating = isBasketProcessing && loaded; + if (!submissionDisabled && isPaypalRedirect) { + // auto submit to paypal since the paypal redirect flag is set in the incoming request + this.handleSubmitPayPal(); + } + // Stripe element config // TODO: Move these to a better home const options = { From 25e7e5d613fd3be9a49c2fbc429ccf16abe49d5f Mon Sep 17 00:00:00 2001 From: Batanayi Matuku Date: Mon, 8 Jul 2024 11:36:27 +0000 Subject: [PATCH 7/9] feat: Added new paypal payment redirect test coverage --- src/payment/data/redux.test.js | 3 +++ 1 file changed, 3 insertions(+) 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, }); }); }); From 3a8fcbee1199c1befd94df8ae634107ee041387a Mon Sep 17 00:00:00 2001 From: Batanayi Matuku Date: Mon, 8 Jul 2024 20:02:54 +0200 Subject: [PATCH 8/9] fix: removed duplicate function --- src/payment/checkout/Checkout.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/payment/checkout/Checkout.jsx b/src/payment/checkout/Checkout.jsx index 34fd45ef0..e06604b9a 100644 --- a/src/payment/checkout/Checkout.jsx +++ b/src/payment/checkout/Checkout.jsx @@ -172,11 +172,6 @@ class Checkout extends React.Component { const isBulkOrder = orderType === ORDER_TYPES.BULK_ENROLLMENT; const isQuantityUpdating = isBasketProcessing && loaded; - if (!submissionDisabled && isPaypalRedirect) { - // auto submit to paypal since the paypal redirect flag is set in the incoming request - this.handleSubmitPayPal(); - } - // Stripe element config // TODO: Move these to a better home const options = { From f740dec75167e7f7c74fa8b70ab176c72c4f45de Mon Sep 17 00:00:00 2001 From: Batanayi Matuku Date: Mon, 8 Jul 2024 21:32:56 +0200 Subject: [PATCH 9/9] fix: paypal redirect to only happen once --- src/payment/checkout/Checkout.jsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/payment/checkout/Checkout.jsx b/src/payment/checkout/Checkout.jsx index e06604b9a..04f63c32a 100644 --- a/src/payment/checkout/Checkout.jsx +++ b/src/payment/checkout/Checkout.jsx @@ -26,17 +26,24 @@ 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(); - this.handleRedirectToPaypal(); } handleRedirectToPaypal = () => { const { loading, isBasketProcessing, isPaypalRedirect } = this.props; + const { hasRedirectedToPaypal } = this.state; const submissionDisabled = loading || isBasketProcessing; - if (!submissionDisabled && isPaypalRedirect) { - // auto submit to paypal since the paypal redirect flag is set in the incoming request + if (!submissionDisabled && isPaypalRedirect && !hasRedirectedToPaypal) { + this.setState({ hasRedirectedToPaypal: true }); this.handleSubmitPayPal(); } }; @@ -172,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 = {