From 8194ffcd07a074a27a45613b7aa2aa880b459dda Mon Sep 17 00:00:00 2001 From: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:04:02 +0530 Subject: [PATCH] feat: Autofill feature (#15437) * feat: Autofill feature * Revert "feat: Autofill feature" This reverts commit ac94257cbb516e140ed550ae804e725ca5d036ca. * feat: store prev response in localStorage * review fixes * add tests * remove unused import --------- Co-authored-by: Hariom Balhara Co-authored-by: unknown --- .../Booker/components/hooks/useBookingForm.ts | 3 + .../components/hooks/useInitialFormValues.ts | 9 ++- .../Booker/utils/lastBookingResponse.test.ts | 65 +++++++++++++++++++ .../Booker/utils/lastBookingResponse.ts | 22 +++++++ .../atoms/booker/BookerWebWrapper.tsx | 4 ++ .../hooks/bookings/useHandleBookEvent.ts | 3 + 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 packages/features/bookings/Booker/utils/lastBookingResponse.test.ts create mode 100644 packages/features/bookings/Booker/utils/lastBookingResponse.ts diff --git a/packages/features/bookings/Booker/components/hooks/useBookingForm.ts b/packages/features/bookings/Booker/components/hooks/useBookingForm.ts index 8a6ec51528..6f318dc6f4 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookingForm.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookingForm.ts @@ -22,6 +22,7 @@ export interface IUseBookingForm { guests: string[]; name: string | null; }; + lastBookingResponse?: Record; } export type UseBookingFormReturnType = ReturnType; @@ -34,6 +35,7 @@ export const useBookingForm = ({ hasSession, extraOptions, prefillFormParams, + lastBookingResponse, }: IUseBookingForm) => { const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const bookingData = useBookerStore((state) => state.bookingData); @@ -71,6 +73,7 @@ export const useBookingForm = ({ hasSession, extraOptions, prefillFormParams, + lastBookingResponse, }); const bookingForm = useForm({ diff --git a/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts b/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts index 89842e9633..434b4aed74 100644 --- a/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts +++ b/packages/features/bookings/Booker/components/hooks/useInitialFormValues.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import type { z } from "zod"; +import { z } from "zod"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import type getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema"; @@ -21,6 +21,7 @@ type UseInitialFormValuesProps = { guests: string[]; name: string | null; }; + lastBookingResponse?: Record; }; export function useInitialFormValues({ @@ -33,6 +34,7 @@ export function useInitialFormValues({ hasSession, extraOptions, prefillFormParams, + lastBookingResponse, }: UseInitialFormValuesProps) { const [initialValues, setDefaultValues] = useState<{ responses?: Partial>>; @@ -62,6 +64,7 @@ export function useInitialFormValues({ // `guests` because the `name` of the corresponding bookingField is `guests` guests: prefillFormParams.guests, }); + const parsedLastBookingResponse = z.record(z.any()).nullish().parse(lastBookingResponse); const defaultUserValues = { email: @@ -69,13 +72,13 @@ export function useInitialFormValues({ ? bookingData?.attendees[0].email : !!parsedQuery["email"] ? parsedQuery["email"] - : email ?? "", + : email ?? parsedLastBookingResponse?.email ?? "", name: rescheduleUid && bookingData && bookingData.attendees.length > 0 ? bookingData?.attendees[0].name : !!parsedQuery["name"] ? parsedQuery["name"] - : name ?? username ?? "", + : name ?? username ?? parsedLastBookingResponse?.name ?? "", }; if (!isRescheduling) { diff --git a/packages/features/bookings/Booker/utils/lastBookingResponse.test.ts b/packages/features/bookings/Booker/utils/lastBookingResponse.test.ts new file mode 100644 index 0000000000..7c107efa86 --- /dev/null +++ b/packages/features/bookings/Booker/utils/lastBookingResponse.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +import { localStorage } from "@calcom/lib/webstorage"; + +import { setLastBookingResponse, getLastBookingResponse } from "./lastBookingResponse"; + +vi.mock("@calcom/lib/webstorage", () => ({ + localStorage: { + getItem: vi.fn(), + setItem: vi.fn(), + }, +})); + +describe("Last Booking Response", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("setLastBookingResponse", () => { + it("should not store anything if responses is null", () => { + setLastBookingResponse(null); + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + + it("should store only allowed responses in localStorage", () => { + const responses = { + email: "test@example.com", + name: "John Doe", + age: 30, + }; + + setLastBookingResponse(responses); + + expect(localStorage.setItem).toHaveBeenCalledWith( + "lastBookingResponse", + JSON.stringify({ email: "test@example.com", name: "John Doe" }) + ); + }); + }); + + describe("getLastBookingResponse", () => { + it("should return an empty object if there is no last booking response", () => { + (localStorage.getItem as jest.Mock).mockReturnValue(null); + + const result = getLastBookingResponse(); + expect(result).toEqual({}); + }); + + it("should return parsed response if valid JSON is present", () => { + const mockResponse = JSON.stringify({ email: "test@example.com", name: "John Doe" }); + (localStorage.getItem as jest.Mock).mockReturnValue(mockResponse); + + const result = getLastBookingResponse(); + expect(result).toEqual({ email: "test@example.com", name: "John Doe" }); + }); + + it("should return an empty object if JSON is invalid", () => { + const invalidJson = "{ email: 'test@example.com' "; + (localStorage.getItem as jest.Mock).mockReturnValue(invalidJson); + + const result = getLastBookingResponse(); + expect(result).toEqual({}); + }); + }); +}); diff --git a/packages/features/bookings/Booker/utils/lastBookingResponse.ts b/packages/features/bookings/Booker/utils/lastBookingResponse.ts new file mode 100644 index 0000000000..d6583353f8 --- /dev/null +++ b/packages/features/bookings/Booker/utils/lastBookingResponse.ts @@ -0,0 +1,22 @@ +import logger from "@calcom/lib/logger"; +import { localStorage } from "@calcom/lib/webstorage"; + +const responsesToStore = ["email", "name"]; + +export const setLastBookingResponse = (responses: Record | null) => { + if (!responses) return; + const prevResponse = Object.fromEntries( + Object.entries(responses).filter(([key]) => responsesToStore.includes(key)) + ); + localStorage.setItem("lastBookingResponse", JSON.stringify(prevResponse)); +}; + +export const getLastBookingResponse = () => { + const lastBookingResponse = localStorage.getItem("lastBookingResponse"); + try { + return JSON.parse(lastBookingResponse ?? "{}"); + } catch (err) { + logger.error("Error parsing lastBookingResponse: ", err); + return {}; + } +}; diff --git a/packages/platform/atoms/booker/BookerWebWrapper.tsx b/packages/platform/atoms/booker/BookerWebWrapper.tsx index 9ff67deeef..3a7900c0e9 100644 --- a/packages/platform/atoms/booker/BookerWebWrapper.tsx +++ b/packages/platform/atoms/booker/BookerWebWrapper.tsx @@ -19,6 +19,7 @@ import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail"; import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event"; +import { getLastBookingResponse } from "@calcom/features/bookings/Booker/utils/lastBookingResponse"; import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors"; import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; @@ -85,6 +86,8 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { }; }, [searchParams, firstNameQueryParam, lastNameQueryParam]); + const lastBookingResponse = getLastBookingResponse(); + const bookerForm = useBookingForm({ event: event.data, sessionEmail: session?.user.email, @@ -93,6 +96,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { hasSession, extraOptions: routerQuery, prefillFormParams, + lastBookingResponse, }); const calendars = useCalendars({ hasSession }); const verifyEmail = useVerifyEmail({ diff --git a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts index 99292226d7..48ff5a136c 100644 --- a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts +++ b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts @@ -1,5 +1,6 @@ import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import { setLastBookingResponse } from "@calcom/features/bookings/Booker/utils/lastBookingResponse"; import { useTimePreferences, mapBookingToMutationInput, @@ -74,6 +75,8 @@ export const useHandleBookEvent = ({ ? duration : event.data.length; + setLastBookingResponse(values.responses); + const bookingInput = { values, duration: validDuration,