diff --git a/src/components/Stepper/CheckoutStepperContainer.tsx b/src/components/Stepper/CheckoutStepperContainer.tsx
index 86259bf5..c343ffb8 100644
--- a/src/components/Stepper/CheckoutStepperContainer.tsx
+++ b/src/components/Stepper/CheckoutStepperContainer.tsx
@@ -1,23 +1,36 @@
+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 => (
- <>
-
-
-
- >
-);
+const Steps = (): ReactElement => {
+ const {
+ FEATURE_SELF_SERVICE_ESSENTIALS,
+ FEATURE_SELF_SERVICE_ESSENTIALS_KEY,
+ } = getConfig();
+ return (
+ <>
+ {
+ isFeatureEnabled(
+ FEATURE_SELF_SERVICE_ESSENTIALS,
+ FEATURE_SELF_SERVICE_ESSENTIALS_KEY,
+ ) &&
+ }
+
+
+
+ >
+ );
+};
const CheckoutStepperContainer = (): ReactElement => {
const { currentStepKey, currentSubstepKey } = useCurrentStep();
-
useEffect(() => {
const preventUnload = (e: BeforeUnloadEvent) => {
if (currentSubstepKey !== CheckoutSubstepKey.Success) {
diff --git a/src/components/Stepper/StepperContent/EssentialsAcademicSelectionContent.tsx b/src/components/Stepper/StepperContent/EssentialsAcademicSelectionContent.tsx
new file mode 100644
index 00000000..99465115
--- /dev/null
+++ b/src/components/Stepper/StepperContent/EssentialsAcademicSelectionContent.tsx
@@ -0,0 +1,14 @@
+import type { UseFormReturn } from 'react-hook-form';
+
+interface EssentialsAcademicSelectionContentProps {
+ form: UseFormReturn;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const EssentialsAcademicSelectionContent = ({ form: _form }: EssentialsAcademicSelectionContentProps) => (
+ <>
+ Essentials Academic Selection
+ >
+);
+
+export default EssentialsAcademicSelectionContent;
diff --git a/src/components/Stepper/StepperContent/index.ts b/src/components/Stepper/StepperContent/index.ts
index 87da8570..920bcce0 100644
--- a/src/components/Stepper/StepperContent/index.ts
+++ b/src/components/Stepper/StepperContent/index.ts
@@ -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';
diff --git a/src/components/Stepper/Steps/EssentialsAcademicSelection.tsx b/src/components/Stepper/Steps/EssentialsAcademicSelection.tsx
new file mode 100644
index 00000000..127bc4b8
--- /dev/null
+++ b/src/components/Stepper/Steps/EssentialsAcademicSelection.tsx
@@ -0,0 +1,8 @@
+import { EssentialsAcademicSelectionPage } from '@/components/academic-selection-page';
+
+// TODO: unnecessary layer of abstraction, just move component logic into this file.
+const EssentialsAcademicSelection: React.FC = () => (
+
+);
+
+export default EssentialsAcademicSelection;
diff --git a/src/components/Stepper/Steps/hooks/useStepperContent.tsx b/src/components/Stepper/Steps/hooks/useStepperContent.tsx
index a44b3756..090799f1 100644
--- a/src/components/Stepper/Steps/hooks/useStepperContent.tsx
+++ b/src/components/Stepper/Steps/hooks/useStepperContent.tsx
@@ -2,6 +2,7 @@ import {
AccountDetailsContent,
BillingDetailsContent,
BillingDetailsSuccessContent,
+ EssentialsAcademicSelectionContent,
PlanDetailsContent,
PlanDetailsLoginContent,
PlanDetailsRegisterContent,
@@ -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,
diff --git a/src/components/Stepper/Steps/index.ts b/src/components/Stepper/Steps/index.ts
index f7006aa5..d77ee853 100644
--- a/src/components/Stepper/Steps/index.ts
+++ b/src/components/Stepper/Steps/index.ts
@@ -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';
diff --git a/src/components/academic-selection-page/EssentialsAcademicSelectionPage.tsx b/src/components/academic-selection-page/EssentialsAcademicSelectionPage.tsx
new file mode 100644
index 00000000..2c2f59d2
--- /dev/null
+++ b/src/components/academic-selection-page/EssentialsAcademicSelectionPage.tsx
@@ -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({
+ mode: 'onTouched',
+ resolver: zodResolver(essentialsAcademicSelectionSchema),
+ defaultValues: essentialsFormData,
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default EssentialsAcademicSelectionPage;
diff --git a/src/components/academic-selection-page/index.ts b/src/components/academic-selection-page/index.ts
new file mode 100644
index 00000000..090744cf
--- /dev/null
+++ b/src/components/academic-selection-page/index.ts
@@ -0,0 +1 @@
+export { default as EssentialsAcademicSelectionPage } from './EssentialsAcademicSelectionPage';
diff --git a/src/components/app/routes/loaders/checkoutStepperLoader.ts b/src/components/app/routes/loaders/checkoutStepperLoader.ts
index f87270f5..3e2f9f8a 100644
--- a/src/components/app/routes/loaders/checkoutStepperLoader.ts
+++ b/src/components/app/routes/loaders/checkoutStepperLoader.ts
@@ -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 {
+ return null;
+}
+
/**
* Route loader for Plan Details page.
*
@@ -169,6 +173,7 @@ async function billingDetailsSuccessLoader(queryClient: QueryClient): Promise Promise> = {
+ EssentialsAcademicSelection: essentialsAcademicSelectionLoader,
PlanDetails: planDetailsLoader,
PlanDetailsLogin: planDetailsLoginLoader,
PlanDetailsRegister: planDetailsRegisterLoader,
diff --git a/src/components/app/routes/loaders/rootLoader.ts b/src/components/app/routes/loaders/rootLoader.ts
index fbc1e94f..2101a671 100644
--- a/src/components/app/routes/loaders/rootLoader.ts
+++ b/src/components/app/routes/loaders/rootLoader.ts
@@ -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.
@@ -87,14 +88,9 @@ const makeRootLoader = (
// Feature flag check
if (routeFeatureKey) {
- const sessionKey = sessionStorage.getItem(SSP_SESSION_KEY);
+ const isUnlocked = isFeatureEnabled(false, routeFeatureKey);
- const isUnlockedBySiteKey = !!FEATURE_SELF_SERVICE_SITE_KEY
- && sessionKey === FEATURE_SELF_SERVICE_SITE_KEY;
-
- 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
@@ -121,11 +117,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;
+ // }
// Fetch basic info about authenticated user from JWT token, and also hydrate it with additional
// information from the `/api/user/v1/accounts/` endpoint. We need access to the
diff --git a/src/components/app/routes/loaders/utils.ts b/src/components/app/routes/loaders/utils.ts
index 7cd2b0a6..c5d73f11 100644
--- a/src/components/app/routes/loaders/utils.ts
+++ b/src/components/app/routes/loaders/utils.ts
@@ -210,6 +210,7 @@ interface PrerequisiteCheck {
* and the route that should be returned if that slice is invalid.
*/
export const prerequisiteSpec: Record>> = {
+ Essentials: [],
PlanDetails: [],
AccountDetails: [
{
diff --git a/src/components/app/routes/tests/rootLoader.test.ts b/src/components/app/routes/tests/rootLoader.test.ts
index 582bc18c..b5512ec5 100644
--- a/src/components/app/routes/tests/rootLoader.test.ts
+++ b/src/components/app/routes/tests/rootLoader.test.ts
@@ -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');
});
@@ -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(
@@ -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');
@@ -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(
@@ -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);
diff --git a/src/components/plan-details-pages/PlanDetailsPage.tsx b/src/components/plan-details-pages/PlanDetailsPage.tsx
index 5fe0f49a..34f22731 100644
--- a/src/components/plan-details-pages/PlanDetailsPage.tsx
+++ b/src/components/plan-details-pages/PlanDetailsPage.tsx
@@ -153,9 +153,9 @@ const PlanDetailsPage = () => {
},
});
- const onSubmitCallbacks: {
- [K in SubmitCallbacks]: (data: PlanDetailsData | PlanDetailsLoginPageData | PlanDetailsRegisterPageData) => void
- } = {
+ const onSubmitCallbacks: Record void> = {
+ [SubmitCallbacks.EssentialsAcademicSelection]: () => {},
[SubmitCallbacks.PlanDetails]: async (data: PlanDetailsData) => {
const { validationDecisions, isValid: isValidAdminEmailField } = await validateFieldDetailed(
'adminEmail',
diff --git a/src/constants/checkout.ts b/src/constants/checkout.ts
index fe701d92..86546d26 100644
--- a/src/constants/checkout.ts
+++ b/src/constants/checkout.ts
@@ -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',
@@ -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}`,
@@ -290,18 +298,13 @@ 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',
@@ -312,6 +315,7 @@ export const EssentialsPageDetails = {
} as const;
export const CheckoutPageDetails: { [K in CheckoutPage]: CheckoutPageDetails } = {
+ ...EssentialsPageDetails,
PlanDetails: {
step: 'PlanDetails',
substep: undefined,
@@ -413,12 +417,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',
diff --git a/src/routes.tsx b/src/routes.tsx
index 9ea48f09..347b4f80 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -10,8 +10,7 @@ import AppShell from '@/components/app/routes/AppShell';
import { makeCheckoutStepperLoader, makeRootLoader } from '@/components/app/routes/loaders';
import RouterFallback from '@/components/app/routes/RouterFallback';
import CheckoutPage from '@/components/checkout-page/CheckoutPage';
-import AcademicSelection from '@/components/essentials-page/AcademicSelection';
-import { authenticatedSteps, CheckoutStepKey, EssentialsStepKey } from '@/constants/checkout';
+import { authenticatedSteps, CheckoutStepKey } from '@/constants/checkout';
import { ErrorPage } from './components/ErrorPage';
@@ -99,34 +98,34 @@ export function getRoutes(queryClient: QueryClient) {
loader: getRouteLoader(makeRootLoader, queryClient),
errorElement: ,
children: [
- {
- path: 'essentials',
- element: (
-
- }>
-
-
-
- ),
- children: [
- {
- index: true,
- element: ,
- },
- {
- path: EssentialsStepKey.AcademicSelection,
- element: (
-
-
-
- ),
- },
- ],
- },
- {
- path: 'essentials/*',
- element: ,
- },
+ // {
+ // path: 'essentials',
+ // element: (
+ //
+ // }>
+ //
+ //
+ //
+ // ),
+ // children: [
+ // {
+ // index: true,
+ // element: ,
+ // },
+ // {
+ // path: EssentialsStepKey.AcademicSelection,
+ // element: (
+ //
+ //
+ //
+ // ),
+ // },
+ // ],
+ // },
+ // {
+ // path: 'essentials/*',
+ // element: ,
+ // },
{
path: '/',
loader: getRouteLoader(makeRootLoader, queryClient),
diff --git a/src/types/types.d.ts b/src/types/types.d.ts
index 98bf60a7..c2950d58 100644
--- a/src/types/types.d.ts
+++ b/src/types/types.d.ts
@@ -63,11 +63,11 @@ declare global {
* ==============================
*/
- type CheckoutStep = 'PlanDetails' | 'AccountDetails' | 'BillingDetails';
+ type CheckoutStep = 'Essentials' | 'PlanDetails' | 'AccountDetails' | 'BillingDetails';
- type CheckoutSubstep = 'Login' | 'Register' | 'Success';
+ type CheckoutSubstep = 'AcademicSelection' | 'Login' | 'Register' | 'Success';
- type CheckoutPage = 'PlanDetails' | 'PlanDetailsLogin' | 'PlanDetailsRegister' | 'AccountDetails' | 'BillingDetails' | 'BillingDetailsSuccess';
+ type CheckoutPage = 'EssentialsAcademicSelection' | 'PlanDetails' | 'PlanDetailsLogin' | 'PlanDetailsRegister' | 'AccountDetails' | 'BillingDetails' | 'BillingDetailsSuccess';
export type CheckoutPageRouteValue = (typeof CheckoutPageRoute)[keyof typeof CheckoutPageRoute];
@@ -88,6 +88,7 @@ declare global {
/**
* Form data types derived from Zod schemas
*/
+ type EssentialAcademicSelectionData = z.infer;
type PlanDetailsData = z.infer;
type PlanDetailsLoginPageData = z.infer;
type PlanDetailsRegisterPageData = z.infer;
@@ -98,6 +99,7 @@ declare global {
* Maps step names to their corresponding data types
*/
interface StepDataMap {
+ 'EssentialsAcademicSelection': Partial;
'PlanDetails': Partial;
'AccountDetails': Partial;
'BillingDetails': Partial;
diff --git a/src/utils/common.ts b/src/utils/common.ts
index a52c4bb3..876139ef 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -1,4 +1,5 @@
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
+import { getConfig } from '@edx/frontend-platform/config';
import { logError } from '@edx/frontend-platform/logging';
import dayjs from 'dayjs';
@@ -109,6 +110,30 @@ const sendEnterpriseCheckoutTrackingEvent = ({
);
};
+/**
+ * Determines whether a feature is enabled.
+ * A feature is enabled if its configuration value is true, or if a valid
+ * feature key or site key is present in the session storage.
+ *
+ * @param {boolean} enabled - The configuration value for the feature.
+ * @param {string|null} featureKey - The specific override key for the feature.
+ * @returns {boolean} True if the feature is enabled; otherwise, false.
+ */
+const isFeatureEnabled = (enabled: boolean, featureKey?: string | null): boolean => {
+ const SSP_SESSION_KEY = 'edx.checkout.self-service-purchasing';
+ const { FEATURE_SELF_SERVICE_SITE_KEY } = getConfig();
+ const sessionKey = sessionStorage.getItem(SSP_SESSION_KEY);
+
+ if (enabled) {
+ return true;
+ }
+
+ const isUnlockedBySiteKey = !!FEATURE_SELF_SERVICE_SITE_KEY && sessionKey === FEATURE_SELF_SERVICE_SITE_KEY;
+ const isUnlockedByFeatureKey = !!featureKey && sessionKey === featureKey;
+
+ return isUnlockedBySiteKey || isUnlockedByFeatureKey;
+};
+
export {
defaultQueryClientRetryHandler,
getComputedStylePropertyCSSVariable,
@@ -117,4 +142,5 @@ export {
serverValidationError,
isExpired,
sendEnterpriseCheckoutTrackingEvent,
+ isFeatureEnabled,
};
diff --git a/src/utils/test/common.test.ts b/src/utils/test/common.test.ts
index 37f564a2..bb244780 100644
--- a/src/utils/test/common.test.ts
+++ b/src/utils/test/common.test.ts
@@ -1,7 +1,19 @@
+import { getConfig } from '@edx/frontend-platform/config';
import dayjs from 'dayjs';
import { CheckoutErrorMessagesByField } from '@/constants/checkout';
-import { defaultQueryClientRetryHandler, isExpired, serverValidationError } from '@/utils/common';
+import {
+ defaultQueryClientRetryHandler,
+ isExpired,
+ isFeatureEnabled,
+ serverValidationError,
+} from '@/utils/common';
+
+jest.mock('@edx/frontend-platform/config', () => ({
+ getConfig: jest.fn(() => ({
+ PUBLIC_PATH: '/',
+ })),
+}));
describe('defaultQueryClientRetryHandler', () => {
it.each([
@@ -107,3 +119,54 @@ describe('isExpired', () => {
expect(isExpired(equalToNow)).toBe(false);
});
});
+
+describe('isFeatureEnabled', () => {
+ const SSP_SESSION_KEY = 'edx.checkout.self-service-purchasing';
+
+ beforeEach(() => {
+ sessionStorage.clear();
+ jest.clearAllMocks();
+ });
+
+ it('returns true when enabled is true', () => {
+ (getConfig as jest.Mock).mockReturnValue({});
+ expect(isFeatureEnabled(true)).toBe(true);
+ });
+
+ it('returns false when enabled is false and no keys are in session', () => {
+ (getConfig as jest.Mock).mockReturnValue({
+ FEATURE_SELF_SERVICE_SITE_KEY: 'site-key',
+ });
+ expect(isFeatureEnabled(false, 'feature-key')).toBe(false);
+ });
+
+ it('returns true when enabled is false but featureKey matches session storage', () => {
+ (getConfig as jest.Mock).mockReturnValue({});
+ sessionStorage.setItem(SSP_SESSION_KEY, 'feature-key');
+ expect(isFeatureEnabled(false, 'feature-key')).toBe(true);
+ });
+
+ it('returns true when enabled is false but siteKey matches session storage', () => {
+ (getConfig as jest.Mock).mockReturnValue({
+ FEATURE_SELF_SERVICE_SITE_KEY: 'site-key',
+ });
+ sessionStorage.setItem(SSP_SESSION_KEY, 'site-key');
+ expect(isFeatureEnabled(false, 'feature-key')).toBe(true);
+ });
+
+ it('returns false when session storage key does not match either featureKey or siteKey', () => {
+ (getConfig as jest.Mock).mockReturnValue({
+ FEATURE_SELF_SERVICE_SITE_KEY: 'site-key',
+ });
+ sessionStorage.setItem(SSP_SESSION_KEY, 'wrong-key');
+ expect(isFeatureEnabled(false, 'feature-key')).toBe(false);
+ });
+
+ it('returns true if enabled is false but siteKey is matched, even if featureKey is null', () => {
+ (getConfig as jest.Mock).mockReturnValue({
+ FEATURE_SELF_SERVICE_SITE_KEY: 'site-key',
+ });
+ sessionStorage.setItem(SSP_SESSION_KEY, 'site-key');
+ expect(isFeatureEnabled(false, null)).toBe(true);
+ });
+});