diff --git a/src/components/FormFields/Field.tsx b/src/components/FormFields/Field.tsx index b10f1b95..8f206ed0 100644 --- a/src/components/FormFields/Field.tsx +++ b/src/components/FormFields/Field.tsx @@ -3,7 +3,6 @@ import { Form } from '@openedx/paragon'; import { CheckCircle, Error as ErrorIcon, } from '@openedx/paragon/icons'; -import { camelCase } from 'lodash-es'; import { forwardRef, useCallback, @@ -11,7 +10,6 @@ import { useRef, } from 'react'; -import { CheckoutStepKey } from '@/components/Stepper/constants'; import useCheckoutFormStore from '@/hooks/useCheckoutFormStore'; import useCurrentStep from '@/hooks/useCurrentStep'; @@ -103,9 +101,9 @@ const DefaultFormControlBase = ( ) => { const intl = useIntl(); const controlRef = useRef(null); - const currentStep = camelCase(useCurrentStep()!); - const formData = useCheckoutFormStore((state) => state.formData[currentStep]); - const currentValue = formData?.[name as CheckoutStepKey]; + const { currentStep } = useCurrentStep(); + const formData = useCheckoutFormStore((state) => state.formData[currentStep!]); + const currentValue = formData?.[name]; const setFormData = useCheckoutFormStore((state) => state.setFormData); const { register } = form; const { onChange } = registerOptions; @@ -117,7 +115,7 @@ const DefaultFormControlBase = ( ...registerOptions, onChange: (event: React.ChangeEvent) => { // @ts-ignore - setFormData(currentStep, { + setFormData(step, { ...formData, [name]: event.target.value, }); diff --git a/src/components/Stepper/Steps/hooks/useStepperContent.tsx b/src/components/Stepper/Steps/hooks/useStepperContent.tsx index db840bde..288f3e1b 100644 --- a/src/components/Stepper/Steps/hooks/useStepperContent.tsx +++ b/src/components/Stepper/Steps/hooks/useStepperContent.tsx @@ -1,5 +1,3 @@ -import { useParams } from 'react-router'; - import { AccountDetailsContent, BillingDetailsContent, @@ -8,24 +6,20 @@ import { PlanDetailsLoginContent, PlanDetailsRegisterContent, } from '@/components/Stepper/StepperContent'; -import { determineStepperStep } from '@/components/Stepper/utils'; -import { CheckoutStepKey, CheckoutSubstepKey } from '@/constants/checkout'; +import { useCurrentPage } from '@/hooks/useCurrentStep'; -const StepperContent = { - [CheckoutStepKey.PlanDetails]: PlanDetailsContent, - [CheckoutSubstepKey.Login]: PlanDetailsLoginContent, - [CheckoutSubstepKey.Register]: PlanDetailsRegisterContent, - [CheckoutStepKey.AccountDetails]: AccountDetailsContent, - [CheckoutStepKey.BillingDetails]: BillingDetailsContent, - [CheckoutSubstepKey.Success]: BillingDetailsSuccessContent, - fallback: () => {}, -}; +const StepperContentByPage = { + PlanDetails: PlanDetailsContent, + PlanDetailsLogin: PlanDetailsLoginContent, + PlanDetailsRegister: PlanDetailsRegisterContent, + AccountDetails: AccountDetailsContent, + BillingDetails: BillingDetailsContent, + BillingDetailsSuccess: BillingDetailsSuccessContent, +} as { [K in CheckoutPage]: React.FC }; const useStepperContent = () => { - const params = useParams<{ step: CheckoutStepKey, substep: CheckoutSubstepKey }>(); - const currentStep = determineStepperStep(params); - // @ts-ignore - return StepperContent[currentStep]; + const currentPage = useCurrentPage(); + return currentPage ? StepperContentByPage[currentPage] : undefined; }; export default useStepperContent; diff --git a/src/components/Stepper/constants.ts b/src/components/Stepper/constants.ts index 5474c156..176b7ee9 100644 --- a/src/components/Stepper/constants.ts +++ b/src/components/Stepper/constants.ts @@ -2,8 +2,10 @@ import { AccountDetailsSchema, BillingDetailsSchema, + CheckoutPageDetails, + CheckoutStepByKey, CheckoutStepKey, - CheckoutStepperPath, + CheckoutSubstepByKey, CheckoutSubstepKey, PlanDetailsLoginSchema, PlanDetailsRegistrationSchema, @@ -21,8 +23,10 @@ export const authenticatedSteps = [ export { AccountDetailsSchema, BillingDetailsSchema, + CheckoutPageDetails, + CheckoutStepByKey, CheckoutStepKey, - CheckoutStepperPath, + CheckoutSubstepByKey, CheckoutSubstepKey, PlanDetailsLoginSchema, PlanDetailsRegistrationSchema, diff --git a/src/components/Stepper/utils.ts b/src/components/Stepper/utils.ts index 3b0c16be..e69de29b 100644 --- a/src/components/Stepper/utils.ts +++ b/src/components/Stepper/utils.ts @@ -1,130 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -import { CheckoutStepKey, CheckoutSubstepKey } from '@/constants/checkout'; - -/** - * Define formatted messages for page titles - * These correspond to the FormattedMessage components used in the page h1 elements - */ -export const titleMessages = defineMessages({ - [CheckoutStepKey.PlanDetails]: { - id: 'checkout.planDetails.title', - defaultMessage: 'Plan Details', - description: 'Title for the plan details step', - }, - [CheckoutSubstepKey.Login]: { - id: 'checkout.planDetailsLogin.title', - defaultMessage: 'Log in to your account', - description: 'Title for the login page in the plan details step', - }, - [CheckoutSubstepKey.Register]: { - id: 'checkout.planDetailsRegistration.title', - defaultMessage: 'Create your Account', - description: 'Title for the registration page in the plan details step', - }, - [CheckoutStepKey.AccountDetails]: { - id: 'checkout.accountDetails.title', - defaultMessage: 'Account Details', - description: 'Title for the account details step', - }, - [CheckoutStepKey.BillingDetails]: { - id: 'checkout.billingDetails.title', - defaultMessage: 'Billing Details', - description: 'Title for the billing details step', - }, - [CheckoutSubstepKey.Success]: { - id: 'checkout.billingDetailsSuccess.title', - defaultMessage: 'Thank you, {firstName}.', - description: 'Title for the success page', - }, -}); - -/** - * Define formatted messages for button text - * These correspond to the FormattedMessage components used in the button elements - */ -export const buttonMessages = defineMessages({ - [CheckoutStepKey.PlanDetails]: { - id: 'checkout.planDetails.continue', - defaultMessage: 'Continue', - description: 'Button label for the next step in the plan details step', - }, - [CheckoutSubstepKey.Login]: { - id: 'checkout.registrationPage.register', - defaultMessage: 'Sign in', - description: 'Button label to register a new user in the plan details step', - }, - [CheckoutSubstepKey.Register]: { - id: 'checkout.registrationPage.register', - defaultMessage: 'Register', - description: 'Button label to register a new user in the plan details step', - }, - [CheckoutStepKey.AccountDetails]: { - id: 'checkout.accountDetails.continue', - defaultMessage: 'Continue', - description: 'Button to go to the next page', - }, - [CheckoutStepKey.BillingDetails]: { - id: 'checkout.billingDetails.purchaseNow', - defaultMessage: 'Purchase Now', - description: 'Button to purchase the subscription product', - }, - [CheckoutSubstepKey.Success]: { - id: 'checkout.billingDetailsSuccess.fallback', - }, -}); - -const determineStepperStep = (params: { step?: CheckoutStepKey, substep?: CheckoutSubstepKey }) => { - const { step, substep } = params; - - switch (step) { - case CheckoutStepKey.PlanDetails: - switch (substep) { - case CheckoutSubstepKey.Login: - return CheckoutSubstepKey.Login; - case CheckoutSubstepKey.Register: - return CheckoutSubstepKey.Register; - default: - return CheckoutStepKey.PlanDetails; - } - case CheckoutStepKey.AccountDetails: - return CheckoutStepKey.AccountDetails; - case CheckoutStepKey.BillingDetails: - switch (substep) { - case CheckoutSubstepKey.Success: - return CheckoutSubstepKey.Success; - default: - return CheckoutStepKey.BillingDetails; - } - default: - return null; - } -}; - -/** - * Determines the appropriate formatted message for the helmet title based on the current step and substep - * - * @param params - Object containing the current step and substep - * @returns The formatted message object to use for the helmet title - */ -const determineStepperTitleText = (params: { step?: CheckoutStepKey, substep?: CheckoutSubstepKey }) => { - const { step, substep } = params; - return titleMessages[determineStepperStep({ step, substep })]; -}; - -/** - * Determines the appropriate formatted message for the button text based on the current step and substep - * - * @param params - Object containing the current step and substep - * @returns The formatted message object to use for the button text - */ -const determineStepperButtonText = (params: { step?: CheckoutStepKey, substep?: CheckoutSubstepKey }) => { - const { step, substep } = params; - return buttonMessages[determineStepperStep({ step, substep })]; -}; - -export { - determineStepperTitleText, - determineStepperButtonText, - determineStepperStep, -}; diff --git a/src/components/plan-details-pages/PlanDetailsPage.tsx b/src/components/plan-details-pages/PlanDetailsPage.tsx index 7096f9bc..910a6c5f 100644 --- a/src/components/plan-details-pages/PlanDetailsPage.tsx +++ b/src/components/plan-details-pages/PlanDetailsPage.tsx @@ -8,19 +8,17 @@ import { } from '@openedx/paragon'; import { Helmet } from 'react-helmet'; import { useForm } from 'react-hook-form'; -import { useParams } from 'react-router'; import { useNavigate } from 'react-router-dom'; import { PriceAlert } from '@/components/PriceAlert'; import useStepperContent from '@/components/Stepper/Steps/hooks/useStepperContent'; -import { determineStepperButtonText, determineStepperStep, determineStepperTitleText } from '@/components/Stepper/utils'; import { + CheckoutPageDetails, CheckoutStepKey, - CheckoutStepperPath, - CheckoutSubstepKey, PlanDetailsSchema, } from '@/constants/checkout'; import useCheckoutFormStore from '@/hooks/useCheckoutFormStore'; +import { useCurrentPageDetails } from '@/hooks/useCurrentStep'; import '../Stepper/Steps/css/PriceAlert.css'; @@ -28,7 +26,6 @@ const PlanDetailsPage: React.FC = () => { // TODO: Example usage of retrieving context data and validation // const bffContext = useBFFContext(); // const bffValidation = useBFFValidation(baseValidation); - const { step, substep } = useParams<{ step: CheckoutStepKey, substep: CheckoutSubstepKey }>(); const intl = useIntl(); const planFormData = useCheckoutFormStore((state) => state.formData.planDetails); const formData = useCheckoutFormStore((state) => state.formData); @@ -57,29 +54,29 @@ const PlanDetailsPage: React.FC = () => { // TODO: replace with an authenticatedUser if (!isAuthenticated) { if (randomExistingEmail) { - navigate(CheckoutStepperPath.LoginRoute); + navigate(CheckoutPageDetails.PlanDetailsLogin.route); } else { - navigate(CheckoutStepperPath.RegisterRoute); + navigate(CheckoutPageDetails.PlanDetailsRegister.route); } return; } if (isAuthenticated) { - navigate(CheckoutStepperPath.AccountDetailsRoute); + navigate(CheckoutPageDetails.AccountDetails.route); } }; + const { title: pageTitle, buttonMessage: stepperActionButtonMessage } = useCurrentPageDetails(); const StepperContent = useStepperContent(); const eventKey = CheckoutStepKey.PlanDetails; - determineStepperStep({ step, substep }); return (

- {intl.formatMessage(determineStepperTitleText({ step, substep }))} + {intl.formatMessage(pageTitle)}

@@ -93,7 +90,7 @@ const PlanDetailsPage: React.FC = () => { disabled={!isValid} data-testid="stepper-submit-button" > - {intl.formatMessage(determineStepperButtonText({ step, substep }))} + {intl.formatMessage(stepperActionButtonMessage)} diff --git a/src/constants/checkout.ts b/src/constants/checkout.ts index 095cda39..ae6b029b 100644 --- a/src/constants/checkout.ts +++ b/src/constants/checkout.ts @@ -1,3 +1,4 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; import { z } from 'zod'; export enum CheckoutStepKey { @@ -12,14 +13,102 @@ export enum CheckoutSubstepKey { Success = 'success', } -export enum CheckoutStepperPath { - PlanDetailsRoute = '/plan-details', - LoginRoute = '/plan-details/login', - RegisterRoute = '/plan-details/register', - AccountDetailsRoute = '/account-details', - BillingDetailsRoute = '/billing-details', - SuccessRoute = '/billing-details/success', -} +export const CheckoutStepByKey = Object.fromEntries( + Object.entries(CheckoutStepKey).map(([key, value]) => [value, key as keyof typeof CheckoutStepKey]), +) as Record; + +export const CheckoutSubstepByKey = Object.fromEntries( + Object.entries(CheckoutSubstepKey).map(([key, value]) => [value, key as keyof typeof CheckoutSubstepKey]), +) as Record; + +export const CheckoutPageDetails = { + PlanDetails: { + step: 'PlanDetails', + substep: undefined, + route: `/${CheckoutStepKey.PlanDetails}`, + title: defineMessages({ + id: 'checkout.planDetails.title', + defaultMessage: 'Plan Details', + description: 'Title for the plan details page', + }), + buttonMessage: defineMessages({ + id: 'checkout.planDetails.continue', + defaultMessage: 'Continue', + description: 'Button label for the next step in the plan details step', + }), + } as CheckoutPageDetails, + PlanDetailsLogin: { + step: 'PlanDetails', + substep: 'Login', + route: `/${CheckoutStepKey.PlanDetails}/${CheckoutSubstepKey.Login}`, + title: defineMessages({ + id: 'checkout.planDetailsLogin.title', + defaultMessage: 'Log in to your account', + description: 'Title for the login page in the plan details step', + }), + buttonMessage: defineMessages({ + id: 'checkout.registrationPage.login', + defaultMessage: 'Log in', + description: 'Button label to login a user in the plan details step', + }), + }, + PlanDetailsRegister: { + step: 'PlanDetails', + substep: 'Register', + route: `/${CheckoutStepKey.PlanDetails}/${CheckoutSubstepKey.Register}`, + title: defineMessages({ + id: 'checkout.planDetailsRegistration.title', + defaultMessage: 'Create your Account', + description: 'Title for the registration page in the plan details step', + }), + buttonMessage: defineMessages({ + id: 'checkout.registrationPage.register', + defaultMessage: 'Sign in', + description: 'Button label to register a new user in the plan details step', + }), + }, + AccountDetails: { + step: 'AccountDetails', + substep: undefined, + route: `/${CheckoutStepKey.AccountDetails}`, + title: defineMessages({ + id: 'checkout.accountDetails.title', + defaultMessage: 'Account Details', + description: 'Title for the account details step', + }), + buttonMessage: defineMessages({ + id: 'checkout.accountDetails.continue', + defaultMessage: 'Continue', + description: 'Button to go to the next page', + }), + }, + BillingDetails: { + step: 'BillingDetails', + substep: undefined, + route: `/${CheckoutStepKey.BillingDetails}`, + title: defineMessages({ + id: 'checkout.billingDetails.title', + defaultMessage: 'Billing Details', + description: 'Title for the billing details step', + }), + buttonMessage: defineMessages({ + id: 'checkout.billingDetails.purchaseNow', + defaultMessage: 'Purchase Now', + description: 'Button to purchase the subscription product', + }), + }, + BillingDetailsSuccess: { + step: 'BillingDetails', + substep: 'Success', + route: `/${CheckoutStepKey.BillingDetails}/${CheckoutSubstepKey.Success}`, + title: defineMessages({ + id: 'checkout.billingDetailsSuccess.title', + defaultMessage: 'Thank you, {firstName}.', + description: 'Title for the success page', + }), + buttonMessage: null, + }, +} as { [K in CheckoutPage]: CheckoutPageDetails }; export const PlanDetailsSchema = z.object({ numUsers: z.coerce.number() diff --git a/src/hooks/useCurrentStep.ts b/src/hooks/useCurrentStep.ts index d04abb45..5f062424 100644 --- a/src/hooks/useCurrentStep.ts +++ b/src/hooks/useCurrentStep.ts @@ -1,10 +1,37 @@ import { useParams } from 'react-router-dom'; -import { CheckoutStepKey } from '@/components/Stepper/constants'; +import { + CheckoutPageDetails, + CheckoutStepByKey, + CheckoutStepKey, + CheckoutSubstepByKey, + CheckoutSubstepKey, +} from '@/components/Stepper/constants'; function useCurrentStep() { - const { step } = useParams<{ step: CheckoutStepKey }>(); - return step; + const { + step: currentStepKey, + substep: currentSubstepKey, + } = useParams<{ step: CheckoutStepKey, substep: CheckoutSubstepKey }>(); + const currentStep = currentStepKey ? CheckoutStepByKey[currentStepKey!] : undefined; + const currentSubstep = currentSubstepKey ? CheckoutSubstepByKey[currentSubstepKey!] : undefined; + return { currentStep, currentStepKey, currentSubstep, currentSubstepKey }; +} + +export function useCurrentPage() { + const { currentStep, currentSubstep } = useCurrentStep(); + const page = Object.keys(CheckoutPageDetails).find(key => ( + CheckoutPageDetails[key].step === currentStep && CheckoutPageDetails[key].substep === currentSubstep + )); + return page; +} + +export function useCurrentPageDetails() { + const currentPage = useCurrentPage(); + if (!currentPage) { + return null; + } + return CheckoutPageDetails[currentPage]; } export default useCurrentStep; diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 2be3dc3c..abb2d49b 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -36,6 +36,20 @@ declare global { * ============================== */ + type CheckoutStep = 'PlanDetails' | 'AccountDetails' | 'BillingDetails'; + + type CheckoutSubstep = 'Login' | 'Register' | 'Success'; + + type CheckoutPage = 'PlanDetails' | 'PlanDetailsLogin' | 'PlanDetailsRegister' | 'AccountDetails' | 'BillingDetails' | 'BillingDetailsSuccess'; + + interface CheckoutPageDetails { + step: CheckoutStep, + substep: CheckoutSubstep | undefined, + route: string, + title: object, + buttonMessage: object | null, + } + /** * Authentication step identifier */