Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: THES-105: Payment State Polling #771

Merged
merged 4 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion audit-ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"GHSA-pfrx-2q88-qq97",
"GHSA-rc47-6667-2j5j",
"GHSA-hc6q-2mpp-qw7j",
"GHSA-f9xv-q969-pqx4"
"GHSA-f9xv-q969-pqx4",
"GHSA-c2qf-rxjj-qqgw",
"GHSA-j8xg-fqg3-53r7"
],
"moderate": true
}
2 changes: 2 additions & 0 deletions src/payment/PaymentPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import EmptyCartMessage from './EmptyCartMessage';
import Cart from './cart/Cart';
import Checkout from './checkout/Checkout';
import { FormattedAlertList } from '../components/formatted-alert-list/FormattedAlertList';
import { PaymentProcessingModal } from './PaymentProcessingModal';

class PaymentPage extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -96,6 +97,7 @@ class PaymentPage extends React.Component {
/>
</h1>
<div className="col-md-5 pr-md-5 col-basket-summary">
<PaymentProcessingModal />
<Cart
isNumEnrolledExperiment={isNumEnrolledExperiment}
REV1045Experiment={REV1045Experiment}
Expand Down
49 changes: 35 additions & 14 deletions src/payment/PaymentProcessingModal.jsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,55 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';

import {
ModalDialog, Spinner,
} from '@edx/paragon';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { ModalDialog, Spinner } from '@edx/paragon';
import { connect, useSelector } from 'react-redux';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';

import messages from './PaymentProcessingModal.messages';
import { paymentProcessStatusSelector } from './data/selectors';
import { paymentProcessStatusIsPollingSelector, paymentProcessStatusSelector } from './data/selectors';
import { POLLING_PAYMENT_STATES } from './data/constants';

/**
* PaymentProcessingModal
*
* This modal is controlled primarily by some Redux selectors.
*
* Controls Visibility: `paymentProcessStatusSelector`, `paymentProcessStatusIsPollingSelector`
* @see paymentProcessStatusSelector
* @see paymentProcessStatusIsPollingSelector
*
* Primary Event: `updatePaymentState`
* @see updatePaymentState
*
* If you wish to perform an action as this dialog closes, please register for the updatePaymentState fulfill event.
*/
export const PaymentProcessingModal = () => {
/**
* Determine if the Dialog should be open based on Redux state input
* @param s {PAYMENT_STATE} The value of the payment state as we currently know it (`paymentProcessStatusSelector`)
* @param p {boolean} is currently polling/still polling for status (`paymentProcessStatusIsPollingSelector`)
* @return {boolean}
*/
const shouldBeOpen = (s, p) => p || POLLING_PAYMENT_STATES.includes(s);

const intl = useIntl();
const shouldBeOpen = (status) => status === 'pending';

const status = useSelector(paymentProcessStatusSelector);
const [isOpen, setOpen] = useState(shouldBeOpen(status));
const isPolling = useSelector(paymentProcessStatusIsPollingSelector);
const [isOpen, setOpen] = useState(shouldBeOpen(status, isPolling));

useEffect(() => {
setOpen(shouldBeOpen(status));
}, [status]);
setOpen(shouldBeOpen(status, isPolling));
}, [status, isPolling]);

if (!isOpen) { return null; }
if (!isOpen) {
return null;
}

return (
<ModalDialog
title="Your Payment is Processing"
isOpen={isOpen}
onClose={() => {}}
onClose={() => { /* Noop, @see updatePaymentState fulfill */ }}
hasCloseButton={false}
isFullscreenOnMobile={false}
>
Expand All @@ -51,4 +72,4 @@ export const PaymentProcessingModal = () => {
);
};

export default PaymentProcessingModal;
export default connect()(injectIntl(PaymentProcessingModal));
33 changes: 23 additions & 10 deletions src/payment/PaymentProcessingModal.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,28 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Provider } from 'react-redux';
import React from 'react';
import { PaymentProcessingModal } from './PaymentProcessingModal';
import { PAYMENT_STATE } from './data/constants';

const basketStoreGenerator = (isProcessing) => (
const basketStoreGenerator = (paymentState = PAYMENT_STATE.DEFAULT, keepPolling = false) => (
{
payment: {
basket: {
loading: true,
loaded: false,
submitting: false,
redirect: false,
isBasketProcessing: isProcessing,
isBasketProcessing: false,
products: [{ sku: '00000' }],
enableStripePaymentProcessor: true,
/** Modified by both getActiveOrder and paymentStatePolling */
// eslint-disable-next-line object-shorthand
paymentState: paymentState,
/** state specific to paymentStatePolling */
paymentStatePolling: {
// eslint-disable-next-line object-shorthand
keepPolling: keepPolling, // TODO: GRM: FIX both debugging items (this is one)
counter: 5, // debugging
},
},
clientSecret: { isClientSecretProcessing: false, clientSecretId: '' },
},
Expand All @@ -29,9 +39,9 @@ const basketStoreGenerator = (isProcessing) => (
}
);

const statusStringToBool = (str) => str === 'pending';
const shouldPoll = (payState) => payState === PAYMENT_STATE.PENDING;

const buildDescription = (tp) => `is ${statusStringToBool(tp.status) ? '' : 'NOT '}shown (status == '${tp.status}')`;
const buildDescription = (tp) => `is ${shouldPoll(tp.status) ? '' : 'NOT '}shown (status == '${tp.status}')`;

/**
* PaymentProcessingModal Test
Expand Down Expand Up @@ -68,18 +78,21 @@ describe('<PaymentProcessingModal />', () => {
},
});

/* eslint-disable no-multi-spaces */
const tests = [
{ expect: true, status: 'pending' },
{ expect: false, status: 'failed' },
{ expect: false, status: 'draft' },
{ expect: false, status: 'checkout' },
{ expect: true, status: PAYMENT_STATE.PENDING },
{ expect: false, status: PAYMENT_STATE.FAILED },
{ expect: false, status: PAYMENT_STATE.DEFAULT },
{ expect: false, status: PAYMENT_STATE.CHECKOUT },
{ expect: false, status: PAYMENT_STATE.COMPLETED },
];
/* eslint-enable no-multi-spaces */

for (let i = 0, testPlan = tests[i]; i < tests.length; i++, testPlan = tests[i]) {
describe(buildDescription(testPlan), () => {
it(`${statusStringToBool(testPlan.status) ? 'renders a dialog' : 'does not render a dialog'}`, () => {
it(`${shouldPoll(testPlan.status) ? 'renders a dialog' : 'does not render a dialog'}`, () => {
const mockStore = configureMockStore();
const store = mockStore(basketStoreGenerator(statusStringToBool(testPlan.status)));
const store = mockStore(basketStoreGenerator(testPlan.status, shouldPoll(testPlan.status)));

const wrapper = mount((
<IntlProvider locale="en">
Expand Down
20 changes: 7 additions & 13 deletions src/payment/checkout/payment-form/StripePaymentForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';

import CardHolderInformation from './CardHolderInformation';
import PlaceOrderButton from './PlaceOrderButton';
import { PaymentProcessingModal } from '../../PaymentProcessingModal';
import SubscriptionSubmitButton from '../../../subscription/checkout/submit-button/SubscriptionSubmitButton';
import MonthlyBillingNotification from '../../../subscription/checkout/monthly-billing-notification/MonthlyBillingNotification';

Expand Down Expand Up @@ -186,18 +185,13 @@ const StripePaymentForm = ({
/>
</>
) : (
// Standard Purchase Flow
<>
{isProcessing && (
<PaymentProcessingModal />
) }
<PlaceOrderButton
onSubmitButtonClick={onSubmitButtonClick}
showLoadingButton={showLoadingButton}
disabled={submitting}
isProcessing={isProcessing}
/>
</>
// Standard Purchase Flow
<PlaceOrderButton
onSubmitButtonClick={onSubmitButtonClick}
showLoadingButton={showLoadingButton}
disabled={submitting}
isProcessing={isProcessing}
/>
)}

</form>
Expand Down
12 changes: 12 additions & 0 deletions src/payment/data/__snapshots__/redux.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Object {
"isBasketProcessing": false,
"loaded": false,
"loading": true,
"paymentState": "checkout",
"paymentStatePolling": Object {
"keepPolling": false,
},
"products": Array [],
"redirect": false,
"submitting": false,
Expand All @@ -28,6 +32,10 @@ Object {
"isBasketProcessing": false,
"loaded": false,
"loading": true,
"paymentState": "checkout",
"paymentStatePolling": Object {
"keepPolling": false,
},
"products": Array [],
"redirect": false,
"submitting": false,
Expand All @@ -50,6 +58,10 @@ Object {
"isBasketProcessing": false,
"loaded": false,
"loading": true,
"paymentState": "checkout",
"paymentStatePolling": Object {
"keepPolling": false,
},
"products": Array [],
"redirect": false,
"submitting": false,
Expand Down
8 changes: 8 additions & 0 deletions src/payment/data/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const fetchActiveOrder = createRoutine('FETCH_ACTIVE_ORDER');
export const addCoupon = createRoutine('ADD_COUPON');
export const removeCoupon = createRoutine('REMOVE_COUPON');
export const updateQuantity = createRoutine('UPDATE_QUANTITY');
export const updatePaymentState = createRoutine('UPDATE_PAYMENT_STATE');
grmartin marked this conversation as resolved.
Show resolved Hide resolved

// Actions and their action creators
export const BASKET_DATA_RECEIVED = 'BASKET_DATA_RECEIVED';
Expand Down Expand Up @@ -75,3 +76,10 @@ export const clientSecretDataReceived = clientSecret => ({
type: CLIENT_SECRET_DATA_RECEIVED,
payload: clientSecret,
});

export const PAYMENT_STATE_DATA_RECEIVED = 'PAYMENT_STATE_DATA_RECEIVED';

export const paymentStateDataReceived = paymentState => ({
type: PAYMENT_STATE_DATA_RECEIVED,
payload: paymentState,
});
55 changes: 55 additions & 0 deletions src/payment/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,58 @@ export const CERTIFICATE_TYPES = {
VERIFIED: 'verified',
CREDIT: 'credit',
};

/**
* Payment State for async payment processing and UI Dialog Control.
*
* @see POLLING_PAYMENT_STATES
*
* @note: This enum is unique to Commerce Coordinator backend.
*/
export const PAYMENT_STATE = (((webserviceEnum = {
// The enum as the WS Sees it.
/**
* Draft (Checkout) Payment
*/
CHECKOUT: 'checkout',
/**
* Payment Complete
*/
COMPLETED: 'completed',
/**
* Server Side Payment Failure
*/
FAILED: 'failed',
/**
* Payment is Pending
*/
PENDING: 'pending',
}) => ({
...webserviceEnum,

// Our Additions

/**
* Default according to Redux initial state. (Should be an alias of an official value)
*
* @see PAYMENT_STATE.CHECKOUT
*/
DEFAULT: webserviceEnum.CHECKOUT,

/**
* An HTTP Error has occurred between the client and server, this should not be sent over the line.
*
* @note **this is custom to the MFE**, thus a Symbol (which JSON usually skips in serialization)
*/
HTTP_ERROR: Symbol('mfe-only_http_error'),
}))());

/**
* An array of payment states that we intend to run polling against
* @type {(string|Symbol)[]} Values from PAYMENT_STATE
* @see PAYMENT_STATE
*/
export const POLLING_PAYMENT_STATES = [
PAYMENT_STATE.PENDING,
PAYMENT_STATE.HTTP_ERROR,
];
Loading