diff --git a/src/components/StatefulButton/StatefulSubscribeButton.tsx b/src/components/StatefulButton/StatefulSubscribeButton.tsx index 67719eaf..113d19fa 100644 --- a/src/components/StatefulButton/StatefulSubscribeButton.tsx +++ b/src/components/StatefulButton/StatefulSubscribeButton.tsx @@ -8,10 +8,10 @@ import { useQueryClient } from '@tanstack/react-query'; import { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useCheckoutIntent } from '@/components/app/data'; +import { useCheckoutIntent, usePolledCheckoutIntent } from '@/components/app/data'; import { termsAndConditions } from '@/components/app/data/constants'; -import { queryBffContext } from '@/components/app/data/queries/queries'; import { patchCheckoutIntent } from '@/components/app/data/services/checkout-intent'; +import { determineExistingSuccessfulCheckoutIntent } from '@/components/app/data/services/context'; import { CheckoutPageRoute, CheckoutSubstepKey, DataStoreKey } from '@/constants/checkout'; import EVENT_NAMES from '@/constants/events'; import { useCheckoutFormStore } from '@/hooks/useCheckoutFormStore'; @@ -59,7 +59,7 @@ const StatefulSubscribeButton = () => { const [errorMessageKey, setErrorMessageKey] = useState('fallback'); const { data: checkoutIntent } = useCheckoutIntent(); const intl = useIntl(); - + const { data: polledCheckoutIntent } = usePolledCheckoutIntent(); const queryClient = useQueryClient(); const navigate = useNavigate(); const { authenticatedUser }: AppContextValue = useContext(AppContext); @@ -104,7 +104,6 @@ const StatefulSubscribeButton = () => { } // Set the button to the appropriate state based on the response. // Stripe responses map 1:1 to button states except for 'default' which is the initial state. - setStatefulButtonState(response.type || 'default'); if (response.type === 'error') { setErrorMessageKey(buttonMessages.error[response.error?.code] ? response.error?.code : 'fallback'); logError( @@ -113,18 +112,18 @@ const StatefulSubscribeButton = () => { } }; + // Visually alter the Subscribe button to a "successful" appearance if the polled intent state becomes successful. + useEffect(() => { + if (statefulButtonState === 'pending' && determineExistingSuccessfulCheckoutIntent(polledCheckoutIntent?.state)) { + setStatefulButtonState('success'); + } + }, [polledCheckoutIntent?.state, navigate, statefulButtonState]); + useEffect(() => { if (statefulButtonState === 'success') { - // If the payment succeeded, update the checkout session status. + // If the payment succeeded from the stripe API, update the checkout session status. if (status.type === 'complete' && status.paymentStatus === 'paid') { setCheckoutSessionStatus(status); - queryClient.invalidateQueries({ - queryKey: queryBffContext( - authenticatedUser.id, - ).queryKey, - }) - .then(data => data) - .catch(error => logError(error)); sendEnterpriseCheckoutTrackingEvent({ checkoutIntentId: checkoutIntent?.id ?? null, eventName: EVENT_NAMES.SUBSCRIPTION_CHECKOUT.PAYMENT_PROCESSED_SUCCESSFULLY, diff --git a/src/components/StatefulButton/tests/StatefulSubscribeButton.test.tsx b/src/components/StatefulButton/tests/StatefulSubscribeButton.test.tsx index f60b43ab..d802d3c9 100644 --- a/src/components/StatefulButton/tests/StatefulSubscribeButton.test.tsx +++ b/src/components/StatefulButton/tests/StatefulSubscribeButton.test.tsx @@ -47,6 +47,7 @@ jest.mock('@/components/app/data', () => ({ data: { id: 'test-intent', country: 'US', state: 'created' }, })), + usePolledCheckoutIntent: jest.fn(() => ({ data: { state: 'paid' } })), })); jest.mock('@/hooks/useCheckoutFormStore', () => ({ @@ -342,7 +343,6 @@ describe('StatefulSubscribeButton', () => { // Wait for the useEffect to trigger after state changes to 'success' await waitFor(() => { expect(mockSetCheckoutSessionStatus).toHaveBeenCalledWith({ type: 'complete', paymentStatus: 'paid' }); - expect(mockInvalidateQueries).toHaveBeenCalled(); expect(jest.mocked(sendEnterpriseCheckoutTrackingEvent)).toHaveBeenCalledWith({ checkoutIntentId: 'test-intent', eventName: EVENT_NAMES.SUBSCRIPTION_CHECKOUT.PAYMENT_PROCESSED_SUCCESSFULLY, diff --git a/src/components/app/data/services/context.ts b/src/components/app/data/services/context.ts index fef4d2c9..e64ab592 100644 --- a/src/components/app/data/services/context.ts +++ b/src/components/app/data/services/context.ts @@ -80,5 +80,6 @@ const fetchCheckoutSuccess = async (): Promise => { export { fetchCheckoutContext, fetchCheckoutSuccess, + determineExistingSuccessfulCheckoutIntent, paymentProcessedCheckoutIntentStates, }; diff --git a/src/components/app/routes/loaders/checkoutStepperLoader.ts b/src/components/app/routes/loaders/checkoutStepperLoader.ts index baa323c2..b2918d09 100644 --- a/src/components/app/routes/loaders/checkoutStepperLoader.ts +++ b/src/components/app/routes/loaders/checkoutStepperLoader.ts @@ -2,7 +2,7 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { QueryClient } from '@tanstack/react-query'; import { redirect } from 'react-router-dom'; -import { queryBffContext } from '@/components/app/data/queries/queries'; +import { queryBffContext, queryBffSuccess, queryCheckoutIntent } from '@/components/app/data/queries/queries'; import { getCheckoutSessionClientSecret, validateFormState } from '@/components/app/routes/loaders/utils'; import { CheckoutPageRoute } from '@/constants/checkout'; import { checkoutFormStore } from '@/hooks/useCheckoutFormStore'; @@ -144,12 +144,31 @@ async function billingDetailsSuccessLoader(queryClient: QueryClient): Promise { }, }); (useCheckoutSessionClientSecret as jest.Mock).mockReturnValue('secret-123'); + (usePolledCheckoutIntent as jest.Mock).mockReturnValue({ + data: { + id: 1, + state: 'pending', + }, + }); (useCheckout as jest.Mock).mockReturnValue({ canConfirm: true, confirm: jest.fn().mockResolvedValue({ type: 'success' }), diff --git a/src/components/plan-details-pages/tests/PlanDetailsPage.test.tsx b/src/components/plan-details-pages/tests/PlanDetailsPage.test.tsx index fe99688d..7c4dd567 100644 --- a/src/components/plan-details-pages/tests/PlanDetailsPage.test.tsx +++ b/src/components/plan-details-pages/tests/PlanDetailsPage.test.tsx @@ -170,7 +170,7 @@ describe('PlanDetailsPage - Admin Email Validation', () => { // Fill in required form fields using proper user interaction const fullNameInput = screen.getByLabelText(/full name/i); const adminEmailInput = screen.getByLabelText(/work email/i); - const quantityInput = screen.getByLabelText(/how many users/i); + const quantityInput = screen.getByLabelText(/number of licenses/i); const countrySelect = screen.getByLabelText(/country of residence/i); await user.type(fullNameInput, 'John Doe'); @@ -222,7 +222,7 @@ describe('PlanDetailsPage - Admin Email Validation', () => { // Fill in required form fields using proper user interaction const fullNameInput = screen.getByLabelText(/full name/i); const adminEmailInput = screen.getByLabelText(/work email/i); - const quantityInput = screen.getByLabelText(/how many users/i); + const quantityInput = screen.getByLabelText(/number of licenses/i); const countrySelect = screen.getByLabelText(/country of residence/i); await user.type(fullNameInput, 'John Doe'); @@ -271,7 +271,7 @@ describe('PlanDetailsPage - Admin Email Validation', () => { // Fill in required form fields using proper user interaction const fullNameInput = screen.getByLabelText(/full name/i); const adminEmailInput = screen.getByLabelText(/work email/i); - const quantityInput = screen.getByLabelText(/how many users/i); + const quantityInput = screen.getByLabelText(/number of licenses/i); const countrySelect = screen.getByLabelText(/country of residence/i); await user.type(fullNameInput, 'John Doe');