Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
32 changes: 23 additions & 9 deletions src/components/Stepper/CheckoutStepperContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import { getConfig } from '@edx/frontend-platform/config';
import { Col, Row, Stack, Stepper } from '@openedx/paragon';
import { ReactElement, useEffect } from 'react';

import { PurchaseSummary } from '@/components/PurchaseSummary';
import { StepperTitle } from '@/components/Stepper/StepperTitle';
import { AccountDetails, BillingDetails, PlanDetails } from '@/components/Stepper/Steps';
import { AccountDetails, BillingDetails, EssentialsAcademicSelection, PlanDetails } from '@/components/Stepper/Steps';
import { CheckoutSubstepKey } from '@/constants/checkout';
import useCurrentStep from '@/hooks/useCurrentStep';
import { isFeatureEnabled } from '@/utils/common';

const Steps = (): ReactElement => (
<>
<PlanDetails />
<AccountDetails />
<BillingDetails />
</>
);
const Steps = (): ReactElement => {
const {
FEATURE_SELF_SERVICE_ESSENTIALS,
FEATURE_SELF_SERVICE_ESSENTIALS_KEY,
} = getConfig();
console.log(isFeatureEnabled(FEATURE_SELF_SERVICE_ESSENTIALS, FEATURE_SELF_SERVICE_ESSENTIALS_KEY));
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A console.log statement is left in the production code. This should be removed or replaced with proper logging using the logging utility from '@edx/frontend-platform/logging'.

Copilot uses AI. Check for mistakes.
return (
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove console.log statement before merging. This debug output should not be committed to production code.

Copilot uses AI. Check for mistakes.
<>
{
isFeatureEnabled(
FEATURE_SELF_SERVICE_ESSENTIALS,
FEATURE_SELF_SERVICE_ESSENTIALS_KEY,
) && <EssentialsAcademicSelection />
Comment on lines +20 to +23
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very important

}
<PlanDetails />
<AccountDetails />
<BillingDetails />
</>
);
};

const CheckoutStepperContainer = (): ReactElement => {
const { currentStepKey, currentSubstepKey } = useCurrentStep();

useEffect(() => {
const preventUnload = (e: BeforeUnloadEvent) => {
if (currentSubstepKey !== CheckoutSubstepKey.Success) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { UseFormReturn } from "react-hook-form";
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement uses double quotes instead of single quotes, which is inconsistent with the project's code style.

Suggested change
import type { UseFormReturn } from "react-hook-form";
import type { UseFormReturn } from 'react-hook-form';

Copilot uses AI. Check for mistakes.

interface EssentialsAcademicSelectionContentProps {
form: UseFormReturn<EssentialAcademicSelectionData>;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent naming: The type is "EssentialAcademicSelectionData" (singular) but should be "EssentialsAcademicSelectionData" (plural) to match the schema name "EssentialsAcademicSelectionSchema" and maintain naming consistency across the codebase.

Suggested change
form: UseFormReturn<EssentialAcademicSelectionData>;
form: UseFormReturn<EssentialsAcademicSelectionData>;

Copilot uses AI. Check for mistakes.
}

const EssentialsAcademicSelectionContent = ({ form }: EssentialsAcademicSelectionContentProps) => {

Check failure on line 7 in src/components/Stepper/StepperContent/EssentialsAcademicSelectionContent.tsx

View workflow job for this annotation

GitHub Actions / lint

'form' is declared but its value is never read.
return (
<>
Essentials Academic Selection
</>
);
};


export default EssentialsAcademicSelectionContent;
1 change: 1 addition & 0 deletions src/components/Stepper/StepperContent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as PlanDetailsRegisterContent } from './PlanDetailsRegisterCont
export { default as AccountDetailsContent } from './AccountDetailsContent';
export { default as BillingDetailsContent } from './BillingDetailsContent';
export { default as BillingDetailsSuccessContent } from './BillingDetailsSuccessContent';
export { default as EssentialsAcademicSelectionContent } from './EssentialsAcademicSelectionContent';
8 changes: 8 additions & 0 deletions src/components/Stepper/Steps/EssentialsAcademicSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { EssentialsAcademicSelectionPage} from "@/components/academic-selection-page";
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement uses inconsistent quote style (double quotes) and spacing. It should match the project's style guide with single quotes and proper spacing.

Suggested change
import { EssentialsAcademicSelectionPage} from "@/components/academic-selection-page";
import { EssentialsAcademicSelectionPage } from '@/components/academic-selection-page';

Copilot uses AI. Check for mistakes.

// TODO: unnecessary layer of abstraction, just move component logic into this file.
const EssentialsAcademicSelection: React.FC = () => (
<EssentialsAcademicSelectionPage />
);

export default EssentialsAcademicSelection;
2 changes: 2 additions & 0 deletions src/components/Stepper/Steps/hooks/useStepperContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AccountDetailsContent,
BillingDetailsContent,
BillingDetailsSuccessContent,
EssentialsAcademicSelectionContent,
PlanDetailsContent,
PlanDetailsLoginContent,
PlanDetailsRegisterContent,
Expand All @@ -12,6 +13,7 @@ import useCurrentPage from '@/hooks/useCurrentPage';
type StepperContentComponent = React.FC<{ form?: any }>;

const StepperContentByPage = {
EssentialsAcademicSelection: EssentialsAcademicSelectionContent,
PlanDetails: PlanDetailsContent,
PlanDetailsLogin: PlanDetailsLoginContent,
PlanDetailsRegister: PlanDetailsRegisterContent,
Expand Down
1 change: 1 addition & 0 deletions src/components/Stepper/Steps/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as PlanDetails } from './PlanDetails';
export { default as AccountDetails } from './AccountDetails';
export { default as BillingDetails } from './BillingDetails';
export { default as EssentialsAcademicSelection } from './EssentialsAcademicSelection';
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Stack, Stepper } from '@openedx/paragon';
import { useMemo } from 'react';
import { Helmet } from 'react-helmet';
import { useForm } from 'react-hook-form';

import { useFormValidationConstraints } from '@/components/app/data';
import { useStepperContent } from '@/components/Stepper/Steps/hooks';
import { CheckoutStepKey, DataStoreKey } from '@/constants/checkout';
import { useCheckoutFormStore, useCurrentPageDetails } from '@/hooks/index';

const EssentialsAcademicSelectionPage = () => {
const StepperContent = useStepperContent();
const { data: formValidationConstraints } = useFormValidationConstraints();
const eventKey = CheckoutStepKey.Essentials;
const {
formSchema,
} = useCurrentPageDetails();
const essentialsFormData = useCheckoutFormStore((state) => state.formData[DataStoreKey.EssentialsAcademicSelection]);

const essentialsAcademicSelectionSchema = useMemo(() => (
formSchema(formValidationConstraints)
), [formSchema, formValidationConstraints]);

const form = useForm<EssentialAcademicSelectionData>({
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent naming: The type is "EssentialAcademicSelectionData" (singular) but should be "EssentialsAcademicSelectionData" (plural) to match the schema name "EssentialsAcademicSelectionSchema" and maintain naming consistency across the codebase.

Suggested change
const form = useForm<EssentialAcademicSelectionData>({
const form = useForm<EssentialsAcademicSelectionData>({

Copilot uses AI. Check for mistakes.
mode: 'onTouched',
resolver: zodResolver(essentialsAcademicSelectionSchema),
defaultValues: essentialsFormData,
});

return (
<>
<Helmet title="Academic Selection Page" />
<Stack gap={4}>
<Stepper.Step eventKey={eventKey} title="Academic Selection Page">
<Stack gap={4}>
<StepperContent form={form} />
</Stack>
</Stepper.Step>
</Stack>
</>
);
};

export default EssentialsAcademicSelectionPage;
Comment on lines +1 to +45
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new EssentialsAcademicSelectionPage component lacks test coverage. Similar page components in the codebase (e.g., AccountDetailsPage) have corresponding test files. Consider adding tests to cover the component's rendering, form validation, and integration with the stepper content.

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions src/components/academic-selection-page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as EssentialsAcademicSelectionPage } from './EssentialsAcademicSelectionPage'
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at the end of the export statement. This is inconsistent with the code style in other index files in the project.

Suggested change
export { default as EssentialsAcademicSelectionPage } from './EssentialsAcademicSelectionPage'
export { default as EssentialsAcademicSelectionPage } from './EssentialsAcademicSelectionPage';

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion src/components/account-details-page/AccountDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Helmet } from 'react-helmet';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';

import AccountDetailsSubmitButton from './AccountDetailsSubmitButton';
import { useCheckoutIntent, useFormValidationConstraints } from '@/components/app/data';
import { useCreateCheckoutSessionMutation } from '@/components/app/data/hooks';
import { queryBffContext, queryBffSuccess } from '@/components/app/data/queries/queries';
Expand All @@ -29,7 +30,6 @@ import {
} from '@/hooks/index';
import { sendEnterpriseCheckoutTrackingEvent } from '@/utils/common';

import AccountDetailsSubmitButton from './AccountDetailsSubmitButton';

const AccountDetailsPage: React.FC = () => {
const { data: formValidationConstraints } = useFormValidationConstraints();
Expand Down
5 changes: 5 additions & 0 deletions src/components/app/routes/loaders/checkoutStepperLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { CheckoutPageRoute, DataStoreKey } from '@/constants/checkout';
import { checkoutFormStore } from '@/hooks/useCheckoutFormStore';
import { extractPriceId, getCheckoutPageDetails, getStepFromParams } from '@/utils/checkout';

async function essentialsAcademicSelectionLoader(): Promise<Response | null> {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The essentialsAcademicSelectionLoader function doesn't match the signature of other loader functions in this file. It should accept a QueryClient parameter for consistency with other loaders (e.g., accountDetailsLoader, billingDetailsLoader) even if it doesn't use it yet. This ensures a consistent interface for all page loaders.

Suggested change
async function essentialsAcademicSelectionLoader(): Promise<Response | null> {
async function essentialsAcademicSelectionLoader(_queryClient: QueryClient): Promise<Response | null> {

Copilot uses AI. Check for mistakes.
return null;
}

/**
* Route loader for Plan Details page.
*
Expand Down Expand Up @@ -169,6 +173,7 @@ async function billingDetailsSuccessLoader(queryClient: QueryClient): Promise<Re
* Page-specific route loaders mapped by checkout page
*/
const PAGE_LOADERS: Record<CheckoutPage, (queryClient: QueryClient) => Promise<Response | null>> = {
EssentialsAcademicSelection: essentialsAcademicSelectionLoader,
PlanDetails: planDetailsLoader,
PlanDetailsLogin: planDetailsLoginLoader,
PlanDetailsRegister: planDetailsRegisterLoader,
Expand Down
18 changes: 8 additions & 10 deletions src/components/app/routes/loaders/rootLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@/components/app/routes/loaders/utils';
import { CheckoutPageRoute, EssentialsPageRoute } from '@/constants/checkout';
import { extractPriceId } from '@/utils/checkout';
import { isFeatureEnabled } from '@/utils/common';

/**
* Factory that creates the root route loader for the Enterprise Checkout MFE.
Expand Down Expand Up @@ -89,12 +90,9 @@ const makeRootLoader = (
if (routeFeatureKey) {
const sessionKey = sessionStorage.getItem(SSP_SESSION_KEY);

const isUnlockedBySiteKey = !!FEATURE_SELF_SERVICE_SITE_KEY
&& sessionKey === FEATURE_SELF_SERVICE_SITE_KEY;
const isUnlocked = isFeatureEnabled(false, routeFeatureKey);

const isUnlockedByRouteKey = sessionKey === routeFeatureKey;

if (!isUnlockedBySiteKey && !isUnlockedByRouteKey) {
if (!isUnlocked) {
const featureParam = new URL(request.url).searchParams.get('feature');

const paramIsSiteKey = !!FEATURE_SELF_SERVICE_SITE_KEY
Expand All @@ -121,11 +119,11 @@ const makeRootLoader = (
* Essentials routes do not participate in checkout intent logic.
* This check happens AFTER feature flag validation.
*/
const isCheckoutRoute = !Object.values(EssentialsPageRoute).some(route => isPathMatch(currentPath, route));

if (!isCheckoutRoute) {
return null;
}
// const isCheckoutRoute = !Object.values(EssentialsPageRoute).some(route => isPathMatch(currentPath, route));
//
// if (!isCheckoutRoute) {
// return null;
// }
Comment on lines +120 to +124
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented-out code block handling checkout intent validation should either be removed or have a clear explanation of why it's being commented out. Leaving large blocks of commented code in production reduces code maintainability. If this is intentional for the POC, consider adding a TODO comment explaining the reasoning.

Copilot uses AI. Check for mistakes.

// Fetch basic info about authenticated user from JWT token, and also hydrate it with additional
// information from the `<LMS>/api/user/v1/accounts/<username>` endpoint. We need access to the
Expand Down
10 changes: 5 additions & 5 deletions src/components/app/routes/tests/rootLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('makeRootLoader (rootLoader) tests', () => {
FEATURE_SELF_SERVICE_ESSENTIALS_KEY: 'essentials-key',
});

const result = getFeatureForPath(EssentialsPageRoute.AcademicSelection);
const result = getFeatureForPath(EssentialsPageRoute.EssentialsAcademicSelection);
expect(result).toBe('essentials-key');
});

Expand Down Expand Up @@ -236,7 +236,7 @@ describe('makeRootLoader (rootLoader) tests', () => {
const loader = makeRootLoader(queryClient);

const result = await loader({
request: makeRequest(`${EssentialsPageRoute.AcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`),
request: makeRequest(`${EssentialsPageRoute.EssentialsAcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`),
} as any);

expect(logInfo).toHaveBeenCalledWith(
Expand All @@ -259,7 +259,7 @@ describe('makeRootLoader (rootLoader) tests', () => {
const loader = makeRootLoader(queryClient);

await expect(
loader({ request: makeRequest(EssentialsPageRoute.AcademicSelection) } as any),
loader({ request: makeRequest(EssentialsPageRoute.EssentialsAcademicSelection) } as any),
).rejects.toThrow('Self-service purchasing is not enabled');

expect(logError).toHaveBeenCalledWith('Self-service purchasing is not enabled');
Expand All @@ -277,7 +277,7 @@ describe('makeRootLoader (rootLoader) tests', () => {
const loader = makeRootLoader(queryClient);

const result = await loader({
request: makeRequest(`${EssentialsPageRoute.AcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`),
request: makeRequest(`${EssentialsPageRoute.EssentialsAcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`),
} as any);

expect(logInfo).toHaveBeenCalledWith(
Expand All @@ -301,7 +301,7 @@ describe('makeRootLoader (rootLoader) tests', () => {

const result = await loader({
request: makeRequest(
`${EssentialsPageRoute.AcademicSelection}?feature=SSP_SITE_CHECKOUT`,
`${EssentialsPageRoute.EssentialsAcademicSelection}?feature=SSP_SITE_CHECKOUT`,
),
} as any);

Expand Down
27 changes: 17 additions & 10 deletions src/constants/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { validateFieldDetailed } from '@/components/app/data/services/validation
import { serverValidationError } from '@/utils/common';

export enum CheckoutStepKey {
Essentials = 'essentials',
PlanDetails = 'plan-details',
AccountDetails = 'account-details',
BillingDetails = 'billing-details',
}

export enum CheckoutSubstepKey {
AcademicSelection = 'academic-selection',
Login = 'login',
Register = 'register',
Success = 'success',
Expand Down Expand Up @@ -279,9 +281,15 @@ export const BillingDetailsSchema = (constraints: CheckoutContextFieldConstraint
// Simple empty schema - no validation needed for coming soon page
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const AcademicSelectionSchema = (constraints: CheckoutContextFieldConstraints) => (z.object({}));
export const EssentialsAcademicSelectionSchema = (constraints: CheckoutContextFieldConstraints) => (z.object({}));

// NEW ROUTES - Essentials flow
export const EssentialsPageRoute = {
EssentialsAcademicSelection: `/${CheckoutStepKey.Essentials}/${CheckoutSubstepKey.AcademicSelection}`,
} as const;

export const CheckoutPageRoute = {
...EssentialsPageRoute,
PlanDetails: `/${CheckoutStepKey.PlanDetails}`,
PlanDetailsLogin: `/${CheckoutStepKey.PlanDetails}/${CheckoutSubstepKey.Login}`,
PlanDetailsRegister: `/${CheckoutStepKey.PlanDetails}/${CheckoutSubstepKey.Register}`,
Expand All @@ -290,18 +298,14 @@ export const CheckoutPageRoute = {
BillingDetailsSuccess: `/${CheckoutStepKey.BillingDetails}/${CheckoutSubstepKey.Success}`,
} as const;

// NEW ROUTES - Essentials flow
export const EssentialsPageRoute = {
AcademicSelection: `/essentials/${EssentialsStepKey.AcademicSelection}`,
} as const;

// NEW PAGE DETAILS - Essentials flow
export const EssentialsPageDetails = {
AcademicSelection: {
step: 'AcademicSelection',
substep: undefined,
formSchema: AcademicSelectionSchema,
route: EssentialsPageRoute.AcademicSelection,
EssentialsAcademicSelection: {
step: 'Essentials',
substep: 'AcademicSelection',
formSchema: EssentialsAcademicSelectionSchema,
route: EssentialsPageRoute.EssentialsAcademicSelection,
title: defineMessages({
id: 'essentials.academicSelection.title',
defaultMessage: 'Academic Selection',
Expand All @@ -312,6 +316,7 @@ export const EssentialsPageDetails = {
} as const;

export const CheckoutPageDetails: { [K in CheckoutPage]: CheckoutPageDetails } = {
...EssentialsPageDetails,
PlanDetails: {
step: 'PlanDetails',
substep: undefined,
Expand Down Expand Up @@ -413,12 +418,14 @@ export const authenticatedSteps = [
] as const;

export enum DataStoreKey {
EssentialsAcademicSelection = 'EssentialsAcademicSelection',
PlanDetails = 'PlanDetails',
AccountDetails = 'AccountDetails',
BillingDetails = 'BillingDetails',
}

export enum SubmitCallbacks {
EssentialsAcademicSelection = 'EssentialsAcademicSelection',
PlanDetails = 'PlanDetails',
PlanDetailsLogin = 'PlanDetailsLogin',
PlanDetailsRegister = 'PlanDetailsRegister',
Expand Down
Loading
Loading