Skip to content

Commit

Permalink
feat: save step 2 state and return to step 1 when missing card details
Browse files Browse the repository at this point in the history
  • Loading branch information
r41ph committed Jan 13, 2025
1 parent 353c8b3 commit fcdc6f4
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { lazy, Suspense } from 'react';
import { lazy, Suspense, useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { msg, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { debounce } from 'lodash';

import Card from 'web-components/src/components/cards/Card';
import CheckboxLabel from 'web-components/src/components/inputs/new/CheckboxLabel/CheckboxLabel';
import SelectTextField from 'web-components/src/components/inputs/new/SelectTextField/SelectTextField';
import TextField from 'web-components/src/components/inputs/new/TextField/TextField';
import QuestionMarkTooltip from 'web-components/src/components/tooltip/QuestionMarkTooltip';
import { Body, Title } from 'web-components/src/components/typography';
import { Title } from 'web-components/src/components/typography';

import {
COUNTRY_LABEL,
Expand All @@ -18,6 +18,9 @@ import {
STATE_LABEL,
} from 'lib/constants/shared.constants';

import { BuyCreditsSchemaTypes } from 'pages/BuyCredits/BuyCredits.types';
import { useMultiStep } from 'components/templates/MultiStepTemplate';

import { AgreePurchaseFormSchemaType } from './AgreePurchaseForm.schema';

const LocationCountryField = lazy(
Expand All @@ -38,13 +41,18 @@ type Props = { retiring: boolean };
export const Retirement = ({ retiring }: Props) => {
const { _ } = useLingui();
const ctx = useFormContext<AgreePurchaseFormSchemaType>();
const { register, formState, control } = ctx;
const { register, formState, control, getValues } = ctx;
const { errors } = formState;
const {
data,
handleSave: updateMultiStepData,
activeStep,
} = useMultiStep<BuyCreditsSchemaTypes>();

const anonymousPurchase = useWatch({
control: control,
name: 'anonymousPurchase',
});
// const anonymousPurchase = useWatch({
// control: control,
// name: 'anonymousPurchase',
// });
const country = useWatch({
control: control,
name: 'country',
Expand All @@ -53,6 +61,59 @@ export const Retirement = ({ retiring }: Props) => {
control: control,
name: 'stateProvince',
});
const retirementReason = useWatch({
control: control,
name: 'retirementReason',
});
const postalCode = useWatch({
control: control,
name: 'postalCode',
});

const { anonymousPurchase, followProject, subscribeNewsletter, agreeErpa } =
getValues();

useEffect(() => {
debounce(() => {
updateMultiStepData(
{
...data,
retirementReason,
postalCode,
},
activeStep,
);
}, 500)();
// Intentionally omit `updateMultiStepData` and `data` from the dependency array
// because including them trigger unnecessary renders.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [retirementReason, postalCode, activeStep]);

useEffect(() => {
updateMultiStepData(
{
...data,
country,
stateProvince,
anonymousPurchase,
followProject,
subscribeNewsletter,
agreeErpa,
},
activeStep,
);
// Intentionally omit `updateMultiStepData` and `data` from the dependency array
// because including them trigger unnecessary renders.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
country,
stateProvince,
anonymousPurchase,
followProject,
subscribeNewsletter,
agreeErpa,
activeStep,
]);

return (
<div className={retiring ? '' : 'hidden'}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useEffect } from 'react';
import { useFormState, useWatch } from 'react-hook-form';
import { DefaultValues, useFormState, useWatch } from 'react-hook-form';
import { msg, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Stripe, StripeElements } from '@stripe/stripe-js';
import { useAtom } from 'jotai';

import CheckboxLabel from 'web-components/src/components/inputs/new/CheckboxLabel/CheckboxLabel';
import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNextButtons/PrevNextButtons';
import { Body } from 'web-components/src/components/typography/Body';

import { cardDetailsMissingAtom } from 'pages/BuyCredits/BuyCredits.atoms';
import AgreeErpaCheckbox from 'components/atoms/AgreeErpaCheckboxNew';
import Form from 'components/molecules/Form/Form';
import { useZodForm } from 'components/molecules/Form/hook/useZodForm';
Expand All @@ -30,6 +32,8 @@ export type AgreePurchaseFormProps = {
country?: string;
stripe?: Stripe | null;
elements?: StripeElements | null;
initialValues?: DefaultValues<AgreePurchaseFormSchemaType>;
isCardPayment?: boolean;
} & TradableProps;

export const AgreePurchaseForm = ({
Expand All @@ -40,19 +44,17 @@ export const AgreePurchaseForm = ({
elements,
goToChooseCredits,
imgSrc,
initialValues,
isCardPayment,
}: AgreePurchaseFormProps) => {
const { _ } = useLingui();
const { handleBack } = useMultiStep();

const { handleBack, handleActiveStep } = useMultiStep();
const [cardDetailsMissing, setCardDetailsMissing] = useAtom(
cardDetailsMissingAtom,
);
const form = useZodForm({
schema: agreePurchaseFormSchema(retiring),
defaultValues: {
country,
anonymousPurchase: false,
followProject: false,
subscribeNewsletter: false,
agreeErpa: false,
},
defaultValues: initialValues,
mode: 'onBlur',
});
const { errors, isValid, isSubmitting } = useFormState({
Expand All @@ -76,6 +78,24 @@ export const AgreePurchaseForm = ({
form.setValue('country', country);
}, [country, form]);

useEffect(() => {
if (isCardPayment && cardDetailsMissing) {
handleActiveStep(1);
}
}, [
handleActiveStep,
isCardPayment,
cardDetailsMissing,
setCardDetailsMissing,
]);

// reset cardDetailsMissing on unmount
useEffect(() => {
return () => {
setCardDetailsMissing(true);
};
}, [setCardDetailsMissing]);

return (
<Form
form={form}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ export const AgreePurchaseFormFiat = (props: Props) => {
const stripe = useStripe();
const elements = useElements();

return <AgreePurchaseForm {...props} stripe={stripe} elements={elements} />;
return (
<AgreePurchaseForm
{...props}
stripe={stripe}
elements={elements}
isCardPayment
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { useEffect, useMemo, useState } from 'react';
import { DefaultValues, useFormState, useWatch } from 'react-hook-form';
import { useLingui } from '@lingui/react';
import { Stripe, StripeElements } from '@stripe/stripe-js';
import { useSetAtom } from 'jotai';

import { PrevNextButtons } from 'web-components/src/components/molecules/PrevNextButtons/PrevNextButtons';
import { UseStateSetter } from 'web-components/src/types/react/useState';

import { cardDetailsMissingAtom } from 'pages/BuyCredits/BuyCredits.atoms';
import { NEXT, PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants';
import {
CardDetails,
Expand Down Expand Up @@ -62,7 +64,7 @@ export const PaymentInfoForm = ({
data,
} = useMultiStep();
const [paymentInfoValid, setPaymentInfoValid] = useState(false);

const setCardDetailsMissing = useSetAtom(cardDetailsMissingAtom);
const form = useZodForm({
schema: paymentInfoFormSchema(paymentOption, wallet),
defaultValues: {
Expand Down Expand Up @@ -110,6 +112,7 @@ export const PaymentInfoForm = ({
<Form
form={form}
onSubmit={async (values: PaymentInfoFormSchemaType) => {
setCardDetailsMissing(false);
const card = paymentOption === PAYMENT_OPTIONS.CARD;
if (card && !stripe) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type ProviderProps<T extends object> = {
steps: Step[];
children?: JSX.Element | JSX.Element[];
withLocalStorage?: boolean;
forceStep?: number;
};

export function MultiStepProvider<T extends object>({
Expand All @@ -97,18 +98,19 @@ export function MultiStepProvider<T extends object>({
steps,
children,
withLocalStorage = true,
}: React.PropsWithChildren<ProviderProps<T>>): JSX.Element {
forceStep,
}: React.PropsWithChildren<ProviderProps<T>>): JSX.Element | null {
// we don't pass initialValues to localStorage
// to avoid persist the initial empty data structure.
// So initially, data (from storage) is `undefined` or
// previously persisted data.
// If undefined, then we return the initialValues
const { data, saveData, removeData } = useStorage<FormData<T>>(
const { data, saveData, removeData, isLoading } = useStorage<FormData<T>>(
formId,
withLocalStorage,
);

const maxAllowedStep = data?.maxAllowedStep || 0;
const maxAllowedStep = forceStep || data?.maxAllowedStep || 0;

const {
activeStep,
Expand All @@ -118,18 +120,10 @@ export function MultiStepProvider<T extends object>({
handleActiveStep,
goNext,
goBack,
} = useSteps(steps.length);
} = useSteps(steps.length, maxAllowedStep);

const [resultStatus, setResultStatus] = React.useState<ResultStatus>();

// initial step on mount
React.useEffect(() => {
if (data?.maxAllowedStep) {
handleActiveStep(data?.maxAllowedStep);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleNext = (): void => {
goNext();
};
Expand Down Expand Up @@ -202,6 +196,11 @@ export function MultiStepProvider<T extends object>({
resultStatus,
};

// Wait to check local storage before rendering component
if (isLoading) {
return null;
}

return (
<MultiStepContext.Provider value={value}>
{children}
Expand Down Expand Up @@ -245,8 +244,8 @@ function calculatePercentComplete(
return (activeStep * 100) / numSteps;
}

function useSteps(numSteps: number): StepManagement {
const [activeStep, setActiveStep] = React.useState(0);
function useSteps(numSteps: number, startStep?: number): StepManagement {
const [activeStep, setActiveStep] = React.useState(startStep || 0);

// derived states from activeStep
const isLastStep = activeStep === numSteps - 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { StepperSection } from './StepperSection';

type MultiStepProps<T extends object> = ProviderProps<T> & {
children: JSX.Element;
forceStep?: number;
} & Pick<OnBoardingSectionProps, 'classes'>;

export function MultiStepTemplate<T extends object>({
Expand All @@ -14,13 +15,15 @@ export function MultiStepTemplate<T extends object>({
children,
withLocalStorage,
classes,
forceStep,
}: MultiStepProps<T>): JSX.Element {
return (
<MultiStepProvider
formId={formId}
steps={steps}
initialValues={initialValues}
withLocalStorage={withLocalStorage}
forceStep={forceStep}
>
<StepperSection classes={classes}>{children}</StepperSection>
</MultiStepProvider>
Expand Down
5 changes: 4 additions & 1 deletion web-marketplace/src/hooks/useStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ interface StorageApi<T> {
data: T | undefined;
saveData: React.Dispatch<T>;
removeData: () => void;
isLoading: boolean;
}

export default function useStorage<T>(
key: string,
withLocalStorage: boolean,
initialValue?: T,
): StorageApi<T> {
const [isLoading, setIsLoading] = useState(true);
const [data, saveData] = useState<T | undefined>(() => {
// this way, as a fn, this initialization happens just once
if (withLocalStorage) {
Expand All @@ -26,6 +28,7 @@ export default function useStorage<T>(
});

useEffect(() => {
setIsLoading(false);
if (withLocalStorage && data) {
const storedValue = localStorage.getItem(key);
const currentValue = storedValue ? JSON.parse(storedValue) : {};
Expand All @@ -52,5 +55,5 @@ export default function useStorage<T>(
}
};

return { data, saveData, removeData };
return { data, saveData, removeData, isLoading };
}
Loading

0 comments on commit fcdc6f4

Please sign in to comment.