Skip to content

fix: multiple widgets for Booker atom #22925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open

fix: multiple widgets for Booker atom #22925

wants to merge 19 commits into from

Conversation

sean-brydon
Copy link
Member

@sean-brydon sean-brydon commented Aug 6, 2025

What does this PR do?

  • Fixes an issue in the Booker atom wherein previously we were not able to use multiple instance of the Booker widget on the same page. If we would use multiple Booker widgets the state would change in both of them even though one of them would be toggled
  • This fix ensures each instance of the Booker atom widget uses a store of its own instead of sharing a global instance that all the widgets can access which was the previous approach

Visual Demo:

Screen.Recording.2025-08-11.at.6.14.02.PM.mov

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • (N/A) I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • (N/A) I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

For the web app these are the steps:

  1. Run the web app in local
  2. Go to the public profile page of user and navigate to one of his events and try to book them via the booker
  3. There should not be any change in the behaviour of the Booker widget for the web app so ideally it should stay the same

For platform these are the steps:

  1. Run the examples app we have here
  2. Go to booking.tsx in the examples app and copy the code for Booker atom here and paste it just under to make another instance of the same Booker widget appear
  3. In both of the Booker atom widgets you need to either comment out the prop allowUpdatingUrlParams or set it to false
  4. Try to change something in one atom and see if the state of the other widget changes or not

Copy link
Contributor

github-actions bot commented Aug 6, 2025

Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted.

Details:

No release type found in pull request title "fix manage test error". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

@keithwillcode keithwillcode added consumer core area: core, team members only labels Aug 6, 2025
Copy link
Contributor

coderabbitai bot commented Aug 6, 2025

Walkthrough

This PR refactors the Booker state management to a context-backed Zustand store. It adds BookerStoreProvider, useBookerStoreContext, and useInitializeBookerStoreContext, and rewires components, hooks, utilities, and tests to use the context instead of the previous global store hook. The store itself is refactored into a factory (createBookerStore), with the default instance still exported for backward compatibility. Selected timeslot is elevated to explicit state and threaded via props to components like BookEventForm and EventMeta; AvailableTimeSlots emits a selection callback. Platform/web wrappers and calendars are wrapped/updated to consume the provider. Tests gain helpers to mock the store per render.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rajiv-demo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

vercel bot commented Aug 6, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
cal ⬜️ Ignored (Inspect) Aug 11, 2025 1:03pm
cal-eu ⬜️ Ignored (Inspect) Aug 11, 2025 1:03pm

@Ryukemeister Ryukemeister changed the title fix manage test error fix: multiple widgets for Booker atom Aug 10, 2025
devin-ai-integration bot and others added 2 commits August 11, 2025 07:01
…t tests

- Create reusable test utility in test-utils.tsx with comprehensive mock store
- Update Booker.test.tsx to use context-based testing approach
- Fix DatePicker tests in both bookings and calendars packages
- Simulate auto-advance behavior for month navigation tests
- All 14 previously failing tests now pass

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>
@Ryukemeister Ryukemeister added the platform Anything related to our platform plan label Aug 11, 2025
@Ryukemeister Ryukemeister marked this pull request as ready for review August 11, 2025 13:03
@Ryukemeister Ryukemeister requested review from a team as code owners August 11, 2025 13:03
@graphite-app graphite-app bot requested a review from a team August 11, 2025 13:03
@dosubot dosubot bot added the 🐛 bug Something isn't working label Aug 11, 2025
Copy link

graphite-app bot commented Aug 11, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (08/11/25)

1 reviewer was added to this PR based on Keith Williams's automation.

Copy link
Contributor

@Ryukemeister Ryukemeister left a comment

Choose a reason for hiding this comment

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

TLDR here's a short summary of the changes made in this PR

  1. useBookerStore is being replaced by a new context called useBookerStoreContext, where each Booker widget has its own store instead of sharing the same global store instance from useBookerStore
  2. hence a lot of variables now fetch values from useBookerStoreContext hook
  3. in a couple of components some values or event handlers are now being accepted as props instead of fetching their values, reason behind it is that we use the same values in the parent component so the child component just accepts the value instead of calling useBookerStoreContext again
  4. however there should be no change in the behaviour of the Booker in the web app, this should only affect the Booker atom which we have in platform

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🔭 Outside diff range comments (9)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)

305-314: Critical: Direct singleton store usage breaks widget isolation.

The error handler still uses useBookerStore.setState() directly, which breaks the isolation between multiple Booker widget instances. When an error occurs in one widget, it would update the state of all widgets on the page.

This needs to use the context-based store's setters instead:

-        useBookerStore.setState({
-          rescheduleUid: error.data?.rescheduleUid,
-        });
-        useBookerStore.setState({
-          bookingData: {
-            uid: error.data?.rescheduleUid,
-            startTime: error.data?.startTime,
-            attendees: error.data?.attendees,
-          } as unknown as GetBookingType,
-        });

You'll need to access the setters from the context:

const setRescheduleUid = useBookerStoreContext((state) => state.setRescheduleUid);
const setBookingData = useBookerStoreContext((state) => state.setBookingData);

Then use them in the error handler. However, since these setters aren't exposed in the current store interface, you may need to add them first.

packages/features/bookings/Booker/components/DatePicker.tsx (2)

96-103: Avoid state updates during render; move to useEffect

Calling moveToNextMonthOnNoAvailability() directly in render can trigger state updates during render when isLoading is false, causing React warnings and potential loops.

-  const { moveToNextMonthOnNoAvailability } = useMoveToNextMonthOnNoAvailability({
+  const { moveToNextMonthOnNoAvailability } = useMoveToNextMonthOnNoAvailability({
     browsingDate,
     nonEmptyScheduleDays,
     onMonthChange,
     isLoading: isLoading ?? true,
   });
-  moveToNextMonthOnNoAvailability();
+  // Run after render to avoid setState during render
+  useEffect(() => {
+    if (!(isLoading ?? true)) {
+      moveToNextMonthOnNoAvailability();
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [browsingDate.format("YYYY-MM"), JSON.stringify(nonEmptyScheduleDays), isLoading]);

145-145: Handle null selectedDate to avoid invalid Dayjs instance

Passing dayjs(selectedDate) when selectedDate is null yields an invalid instance. Pass null explicitly to keep downstream logic clean.

-      selected={dayjs(selectedDate)}
+      selected={selectedDate ? dayjs(selectedDate) : null}
packages/features/bookings/Booker/components/hooks/useSlots.ts (1)

143-155: Fix stale closure in reservation interval when duration changes

The effect depends on event?.id and timeSlotToBeBooked, but handleReserveSlot also uses eventDuration. If the user changes duration after selecting a slot, the interval will keep using the old duration.

-  }, [event?.id, timeSlotToBeBooked]);
+  }, [event?.id, timeSlotToBeBooked, eventDuration]);

Optionally, for tighter correctness without frequent interval resets, store latest values in refs and read them inside the interval callback. I can provide that refactor if you prefer.

packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (2)

160-170: Localize the button label inside ServerTrans

Direct text “Please select a new time” violates the localization guideline. Use t() or let the i18n key provide the label.

-                    <button
+                    <button
                       key="please-select-a-new-time-button"
                       type="button"
                       className="underline"
                       onClick={onCancel}>
-                      Please select a new time
+                      {t("please_select_a_new_time")}
                     </button>,

If timeslot_unavailable_book_a_new_time already includes the button text, pass an empty button and let the translation control the content:

-                    <button ...>{t("please_select_a_new_time")}</button>,
+                    <button ... />

176-198: Localize anchor labels for Terms and Privacy Policy

Maintain consistency with the later block that uses t("terms") and t("privacy_policy").

-                  Terms
+                  {t("terms")}
@@
-                  Privacy Policy.
+                  {t("privacy_policy")}
packages/platform/atoms/booker/BookerWebWrapper.tsx (1)

187-191: Add error handling for localStorage operations

localStorage operations can fail in certain environments (e.g., private browsing, storage quota exceeded). Consider wrapping these calls in try-catch blocks.

       if (state) {
         current.set("overlayCalendar", "true");
-        localStorage.setItem("overlayCalendarSwitchDefault", "true");
+        try {
+          localStorage.setItem("overlayCalendarSwitchDefault", "true");
+        } catch (e) {
+          console.warn("Failed to persist overlay calendar preference:", e);
+        }
       } else {
         current.delete("overlayCalendar");
-        localStorage.removeItem("overlayCalendarSwitchDefault");
+        try {
+          localStorage.removeItem("overlayCalendarSwitchDefault");
+        } catch (e) {
+          console.warn("Failed to remove overlay calendar preference:", e);
+        }
       }
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (2)

111-128: Critical: still subscribing to the legacy global store

This effect uses useBookerStore.subscribe and useBookerStore.getState, tying onBookerStateChange to the singleton store. With multiple widgets, callbacks will receive state changes from other instances.

Refactor to derive values from the context store state instead. One approach: select the entire store state via context, filter out functions, and debounce callbacks. Example diff:

-  const prevStateRef = useRef<BookerStoreValues | null>(null);
-  const getStateValues = useCallback(
-    (state: ReturnType<typeof useBookerStore.getState>): BookerStoreValues => {
-      return Object.fromEntries(
-        Object.entries(state).filter(([_, value]) => typeof value !== "function")
-      ) as BookerStoreValues;
-    },
-    []
-  );
-  const debouncedStateChange = useMemo(() => {
+  const prevStateRef = useRef<BookerStoreValues | null>(null);
+  const getStateValues = useCallback(
+    (state: any): BookerStoreValues => {
+      return Object.fromEntries(
+        Object.entries(state).filter(([_, value]) => typeof value !== "function")
+      ) as BookerStoreValues;
+    },
+    []
+  );
+  const debouncedStateChange = useMemo(() => {
     return debounce(
       (currentStateValues: BookerStoreValues, callback: (values: BookerStoreValues) => void) => {
         const prevState = prevStateRef.current;
         const stateChanged = !prevState || JSON.stringify(prevState) !== JSON.stringify(currentStateValues);
 
         if (stateChanged) {
           callback(currentStateValues);
           prevStateRef.current = currentStateValues;
         }
       },
       50
     );
   }, []);
 
-  useEffect(() => {
-    if (!onBookerStateChange) return;
-
-    const unsubscribe = useBookerStore.subscribe((state) => {
-      const currentStateValues = getStateValues(state);
-      debouncedStateChange(currentStateValues, onBookerStateChange);
-    });
-
-    // Initial call with current state
-    const initialState = getStateValues(useBookerStore.getState());
-    onBookerStateChange(initialState);
-    prevStateRef.current = initialState;
-
-    return () => {
-      unsubscribe();
-      debouncedStateChange.cancel();
-    };
-  }, [onBookerStateChange, getStateValues, debouncedStateChange]);
+  // Read full state snapshot from the context store
+  const contextStoreState = useBookerStoreContext((s) => s);
+
+  useEffect(() => {
+    if (!onBookerStateChange) return;
+    const currentValues = getStateValues(contextStoreState);
+    debouncedStateChange(currentValues, onBookerStateChange);
+    return () => {
+      debouncedStateChange.cancel();
+    };
+  }, [contextStoreState, onBookerStateChange, getStateValues, debouncedStateChange]);

And drop the legacy import:

- import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
+ // remove useBookerStore import

If you prefer subscribe semantics for fine-grained control, consider exporting a useBookerStoreApi() from the provider to access the underlying StoreApi for subscribe within the provider context.


17-17: Remove legacy store imports to avoid accidental coupling

The file still imports useBookerStore and useInitializeBookerStore from the legacy store. Remove them once the above refactors are applied to prevent regressions back to the global singleton.

-import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
+// Legacy store imports removed; using context-based store exclusively
🧹 Nitpick comments (9)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (5)

13-13: React import is optional with the new JSX runtime

If your tsconfig uses "jsx": "react-jsx", you can drop this import. Harmless either way.


23-23: Avoid any in the framer-motion mock

You can preserve types by using typeof import("framer-motion"). Also, since the mock returns the actual module unchanged, consider removing the mock if it’s not needed.

-  const actual = (await importOriginal()) as any;
+  const actual = (await importOriginal()) as typeof import("framer-motion");
   return {
     ...actual,
   };

If this mock is truly a no-op in your environment, you can delete the entire vi.mock block to reduce noise.


196-203: Timezone-stable selectedDate

You’re mixing a date-only value ("2024-01-01") with UTC timestamps ("...Z"). If selectedDate is parsed relative to local TZ anywhere, this can lead to flaky tests. Consider normalizing to UTC or using a date-only parsing path explicitly.

Example:

-        selectedDate: "2024-01-01",
+        // Keep tests TZ-agnostic; use an explicit UTC start-of-day if the code parses selectedDate as a Date
+        selectedDate: "2024-01-01T00:00:00Z",

Or ensure the code treats selectedDate as a plain date string (no implicit Date parsing).


38-38: Silence noisy console in test mock

The console.log in BookEventForm mock adds noise to CI logs.

-    console.log("BookEventForm Called", { isTimeslotUnavailable, onCancel });
+    // Intentionally silent to keep test logs clean

223-223: Remove debugging helper from test run

screen.logTestingPlaygroundURL() is great locally but noisy in CI.

-    screen.logTestingPlaygroundURL();
+    // screen.logTestingPlaygroundURL();
.changeset/fresh-rivers-glow.md (1)

5-5: Fix grammatical error in changeset description.

There's a typo in the description.

-Fixes an issues for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others.
+Fixes an issue for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others.
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)

305-314: Consider the approach for error state management.

Since rescheduleUid is typically set through the initialize function and there's no dedicated setter, you might need to either:

  1. Add explicit setters for rescheduleUid and update the store interface
  2. Use a different pattern for error recovery that doesn't involve direct state manipulation
  3. Store error-specific state separately from the main booking state
packages/features/calendars/__tests__/DatePicker.test.tsx (1)

20-35: Reduce wrapper repetition via a local render helper

Minor DX: factor the nested providers into a small helper to avoid duplication and ease future changes.

+const renderWithProviders = (ui: React.ReactNode) =>
+  render(
+    <BookerStoreProvider>
+      <TooltipProvider>{ui}</TooltipProvider>
+    </BookerStoreProvider>
+  );
@@
-    const { getByTestId } = render(
-      <BookerStoreProvider>
-        <TooltipProvider>
-          <DatePicker
-            onChange={noop}
-            browsingDate={testDate}
-            locale="en"
-            periodData={{
-              periodType: PeriodType.UNLIMITED,
-              periodDays: null,
-              periodCountCalendarDays: false,
-              periodStartDate: null,
-              periodEndDate: null,
-            }}
-          />
-        </TooltipProvider>
-      </BookerStoreProvider>
-    );
+    const { getByTestId } = renderWithProviders(
+      <DatePicker
+        onChange={noop}
+        browsingDate={testDate}
+        locale="en"
+        periodData={{
+          periodType: PeriodType.UNLIMITED,
+          periodDays: null,
+          periodCountCalendarDays: false,
+          periodStartDate: null,
+          periodEndDate: null,
+        }}
+      />
+    );

Also applies to: 46-51, 61-66, 95-113, 139-157, 179-197

packages/platform/atoms/booker/BookerPlatformWrapper.tsx (1)

507-518: Non-localized error strings (informational)

These thrown "Function not implemented." strings are developer-facing, not user-visible. No action required, but if they can bubble to UI, wrap with t() per guidelines for TSX files.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebeb008 and 4a259b3.

📒 Files selected for processing (21)
  • .changeset/fresh-rivers-glow.md (1 hunks)
  • packages/features/bookings/Booker/Booker.tsx (7 hunks)
  • packages/features/bookings/Booker/BookerStoreProvider.tsx (1 hunks)
  • packages/features/bookings/Booker/__tests__/Booker.test.tsx (5 hunks)
  • packages/features/bookings/Booker/__tests__/test-utils.tsx (1 hunks)
  • packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (5 hunks)
  • packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (4 hunks)
  • packages/features/bookings/Booker/components/DatePicker.test.tsx (5 hunks)
  • packages/features/bookings/Booker/components/DatePicker.tsx (2 hunks)
  • packages/features/bookings/Booker/components/EventMeta.tsx (3 hunks)
  • packages/features/bookings/Booker/components/Header.tsx (2 hunks)
  • packages/features/bookings/Booker/components/hooks/useBookings.ts (2 hunks)
  • packages/features/bookings/Booker/components/hooks/useSlots.ts (2 hunks)
  • packages/features/bookings/Booker/store.ts (2 hunks)
  • packages/features/bookings/Booker/utils/event.ts (3 hunks)
  • packages/features/calendars/DatePicker.tsx (3 hunks)
  • packages/features/calendars/__tests__/DatePicker.test.tsx (7 hunks)
  • packages/platform/atoms/booker/BookerPlatformWrapper.tsx (5 hunks)
  • packages/platform/atoms/booker/BookerWebWrapper.tsx (3 hunks)
  • packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (2 hunks)
  • packages/platform/atoms/hooks/useSlots.ts (4 hunks)
🔥 Files not summarized due to errors (1)
  • packages/features/bookings/Booker/tests/Booker.test.tsx: Error: Server error: no LLM provider could handle the message
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/bookings/Booker/components/hooks/useBookings.ts
  • packages/features/bookings/Booker/components/hooks/useSlots.ts
  • packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
  • packages/platform/atoms/hooks/useSlots.ts
  • packages/features/bookings/Booker/utils/event.ts
  • packages/features/bookings/Booker/store.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/bookings/Booker/components/hooks/useBookings.ts
  • packages/features/bookings/Booker/components/hooks/useSlots.ts
  • packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
  • packages/features/bookings/Booker/__tests__/test-utils.tsx
  • packages/features/calendars/__tests__/DatePicker.test.tsx
  • packages/features/bookings/Booker/components/DatePicker.tsx
  • packages/features/bookings/Booker/components/Header.tsx
  • packages/features/bookings/Booker/BookerStoreProvider.tsx
  • packages/platform/atoms/hooks/useSlots.ts
  • packages/features/bookings/Booker/utils/event.ts
  • packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
  • packages/features/bookings/Booker/components/DatePicker.test.tsx
  • packages/features/calendars/DatePicker.tsx
  • packages/features/bookings/Booker/components/EventMeta.tsx
  • packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
  • packages/features/bookings/Booker/__tests__/Booker.test.tsx
  • packages/features/bookings/Booker/Booker.tsx
  • packages/platform/atoms/booker/BookerWebWrapper.tsx
  • packages/features/bookings/Booker/store.ts
  • packages/platform/atoms/booker/BookerPlatformWrapper.tsx
**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/bookings/Booker/__tests__/test-utils.tsx
  • packages/features/calendars/__tests__/DatePicker.test.tsx
  • packages/features/bookings/Booker/components/DatePicker.tsx
  • packages/features/bookings/Booker/components/Header.tsx
  • packages/features/bookings/Booker/BookerStoreProvider.tsx
  • packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
  • packages/features/bookings/Booker/components/DatePicker.test.tsx
  • packages/features/calendars/DatePicker.tsx
  • packages/features/bookings/Booker/components/EventMeta.tsx
  • packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
  • packages/features/bookings/Booker/__tests__/Booker.test.tsx
  • packages/features/bookings/Booker/Booker.tsx
  • packages/platform/atoms/booker/BookerWebWrapper.tsx
  • packages/platform/atoms/booker/BookerPlatformWrapper.tsx
🧬 Code Graph Analysis (19)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/__tests__/test-utils.tsx (3)
packages/features/bookings/Booker/store.ts (1)
  • BookerStore (51-174)
packages/embeds/embed-core/src/types.ts (1)
  • BookerLayouts (6-6)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • BookerStoreContext (9-9)
packages/features/calendars/__tests__/DatePicker.test.tsx (4)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • BookerStoreProvider (15-22)
packages/platform/atoms/src/components/ui/tooltip.tsx (1)
  • TooltipProvider (14-14)
packages/features/bookings/Booker/components/DatePicker.tsx (1)
  • DatePicker (54-153)
packages/platform/libraries/index.ts (1)
  • PeriodType (30-30)
packages/features/bookings/Booker/components/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/Header.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
packages/features/bookings/Booker/store.ts (3)
  • BookerStore (51-174)
  • createBookerStore (179-446)
  • StoreInitializeType (18-42)
packages/platform/atoms/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/utils/event.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/calendars/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/EventMeta.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (2)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/embeds/embed-core/src/types.ts (1)
  • BookerLayouts (6-6)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (1)
packages/features/bookings/Booker/Booker.tsx (1)
  • Booker (593-599)
packages/features/bookings/Booker/Booker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/platform/atoms/booker/BookerWebWrapper.tsx (4)
packages/platform/atoms/booker/index.ts (2)
  • BookerWebWrapperAtomProps (2-2)
  • BookerWebWrapper (1-1)
packages/features/bookings/Booker/types.ts (1)
  • BookerProps (19-108)
packages/features/eventtypes/lib/getPublicEvent.ts (1)
  • getPublicEvent (254-564)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • BookerStoreProvider (15-22)
packages/features/bookings/Booker/store.ts (3)
packages/features/bookings/Booker/types.ts (2)
  • BookerState (154-154)
  • BookerLayout (155-155)
packages/embeds/embed-core/src/types.ts (1)
  • BookerLayouts (6-6)
packages/lib/constants.ts (1)
  • BOOKER_NUMBER_OF_DAYS_TO_LOAD (170-173)
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (3)
packages/features/bookings/Booker/BookerStoreProvider.tsx (3)
  • useBookerStoreContext (24-35)
  • useInitializeBookerStoreContext (37-113)
  • BookerStoreProvider (15-22)
packages/trpc/server/routers/publicViewer/procedures/event.ts (1)
  • event (8-12)
packages/platform/atoms/booker/types.ts (2)
  • BookerPlatformWrapperAtomPropsForIndividual (92-96)
  • BookerPlatformWrapperAtomPropsForTeam (98-103)
🔇 Additional comments (35)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (4)

169-169: Flags initialization looks fine

Using string values for constants aligns with typical flag infra. No changes needed.


177-179: LGTM: loading state test

Solid assertion that container is empty during loading.


220-222: LGTM: cancel flow invalidation

Good coverage to assert schedule.invalidate() and clearing the selected timeslot on cancel when the flag is enabled.


242-244: LGTM: quick availability check -> unavailable

Covers the unavailability mapping correctly via prop-driven quick checks.

packages/features/bookings/Booker/utils/event.ts (1)

3-3: LGTM! Proper migration to context-based store.

The migration from useBookerStore to useBookerStoreContext is correctly implemented in both hooks, maintaining the shallow comparison for performance optimization.

Also applies to: 22-25, 92-95

packages/features/bookings/Booker/components/Header.tsx (1)

7-7: LGTM! Clean migration to context-based store.

The Header component has been properly migrated to use useBookerStoreContext with appropriate shallow comparison for performance optimization.

Also applies to: 39-42

packages/features/calendars/__tests__/DatePicker.test.tsx (1)

7-7: Correctly wrapped tests with BookerStoreProvider

Wrapping DatePicker with BookerStoreProvider (outside TooltipProvider) is necessary after migrating calendars/DatePicker to read Booker context. Wrapper order is correct and prevents runtime “must be used within BookerStoreProvider” errors.

Also applies to: 20-35, 46-51, 61-66, 95-113, 139-157, 179-197

packages/features/calendars/DatePicker.tsx (1)

7-7: Context migration looks correct

Replacing useBookerStore with useBookerStoreContext for selectedDatesAndTimes and bookingData is consistent with the provider-based architecture. Selectors and shallow equality are preserved.

Also applies to: 227-227, 395-396

packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)

4-5: Migration to useBookerStoreContext is consistent

Selectors map 1:1 to the previous store keys. The bookingInput and flows (instant, recurring, normal) remain unchanged. No issues spotted.

Also applies to: 49-66

packages/features/bookings/Booker/components/DatePicker.test.tsx (1)

1-1: Solid test updates with mock store seeding

  • Switching to the shared test-utils render simplifies provider setup.
  • Fixing dayjs().add(1, "month") is correct.
  • Seeding mockStore.month aligns expectations for browsingDate.

Looks good.

Also applies to: 7-8, 20-23, 62-82, 93-109, 114-115

packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (1)

46-47: timeslot prop introduction is correct and improves clarity

Accepting timeslot via props and gating the form until it’s set eliminates hidden coupling with the store. Early return with EmptyScreen is appropriate.

Also applies to: 66-67, 92-104

packages/features/bookings/Booker/components/EventMeta.tsx (1)

58-59: EventMeta context migration and selectedTimeslot display look good

  • selectedTimeslot prop is correctly used for FromToTime.
  • setTimezone updates both user prefs and Booker store, preserving prior behavior.

Also applies to: 95-101, 206-216, 247-251

packages/features/bookings/Booker/__tests__/test-utils.tsx (2)

103-105: Well-structured test utilities!

The test utilities follow best practices by:

  • Providing a custom render function that includes the necessary context
  • Re-exporting testing-library utilities for convenience
  • Aliasing renderWithBookerStore as render for familiar API usage

80-87: Potential state mutation issue in mock setState

The setState implementation directly modifies the state object before spreading, which could cause issues with reference equality checks in tests.

Apply this fix to ensure proper immutability:

   setState: vi.fn((updater) => {
     if (typeof updater === "function") {
       const newState = updater(state);
-      state = { ...state, ...newState };
+      state = { ...newState };
     } else {
-      state = { ...state, ...updater };
+      state = { ...state, ...updater };
     }
   }),

Likely an incorrect or invalid review comment.

packages/platform/atoms/hooks/useSlots.ts (1)

39-43: Successful migration to context-based store!

All store accesses have been correctly migrated from the singleton pattern to the context-based approach. The shallow equality check for the multi-value selector is properly preserved.

Also applies to: 55-56, 88-88

packages/features/bookings/Booker/BookerStoreProvider.tsx (2)

15-22: Correct provider implementation pattern!

The provider correctly uses useRef to ensure the store is created only once and persists across re-renders, following Zustand's recommended pattern for context-based stores.


24-35: Well-implemented context hook with proper error handling!

The hook correctly validates context availability and provides a clear error message when used outside the provider. The selector pattern with optional equality function follows Zustand best practices.

packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (2)

79-89: Clean migration to context-based store access!

All store accesses have been properly migrated to use useBookerStoreContext with appropriate selectors.


52-52: Excellent architectural improvement with callback pattern!

The introduction of onAvailableTimeSlotSelect callback properly lifts state management to the parent component, improving component reusability and testability. This aligns well with the migration to context-based state management.

Also applies to: 151-151, 167-167

packages/platform/atoms/booker/BookerWebWrapper.tsx (1)

270-276: Clean separation of concerns with provider pattern!

The restructuring properly separates the store provisioning (in BookerWebWrapper) from the component logic (in BookerPlatformWrapperComponent), maintaining the same public API while enabling context-based state management.

packages/features/bookings/Booker/Booker.tsx (9)

13-13: Good: switched to context-based store access

Importing useBookerStoreContext aligns with the per-instance provider and the PR goal to avoid a shared global store.


87-90: Correct: tuple selector with shallow equality

Using a tuple selector with shallow is the right pattern for Zustand to avoid unnecessary re-renders while reading [state, setState].


92-93: LGTM: selectedDate setters via context

Clean and idiomatic; no concerns.


107-110: LGTM: seatedEventData selector pair

Consistent with the rest of the migration.


112-115: LGTM: dayCount selector pair

Consistent with other selectors; using shallow is appropriate.


194-197: Wiring timeslot selection through the store

onAvailableTimeSlotSelect correctly forwards the selection to the context store.


234-234: Verify downstream prop type for timeslot

Confirm BookEventForm’s new prop is typed as timeslot: string | null and is used safely when null.

Would you like me to scan usages of BookEventForm to ensure all call sites pass the new prop?


407-407: Propagate selectedTimeslot to EventMeta

Looks good; verify EventMeta props were updated accordingly to accept selectedTimeslot: string | null.

Do you want a quick repository check to confirm EventMeta’s prop signature?


492-492: AvailableTimeSlots: new selection callback

Passing onAvailableTimeSlotSelect ensures the parent owns the state; this is consistent with the context-backed design.

packages/platform/atoms/booker/BookerPlatformWrapper.tsx (6)

9-13: Good: provider and context hooks imported

This sets up the path to per-instance store isolation.


48-50: Component extraction is fine

Internal component organization is OK and keeps the public API intact.


71-83: Good: reading/writing state via context selectors

Using useBookerStoreContext with shallow tuples for [state, setter] is correct and performant.


197-202: LGTM: reading dayCount/selectedDate/month/eventSlug from context

These reads are correctly aligned with the provider-based store.


445-452: Minor: ensure URL-param updates obey allowUpdatingUrlParams in context initializer

setSelectedDate({ date: selectedDateProp, omitUpdatingParams: true }) is explicit here, which is good. Verify the context store initializer respects allowUpdatingUrlParams to avoid cross-instance URL mutations when multiple widgets exist.

Would you like a quick check across BookerStoreProvider.tsx/store.ts to assert allowUpdatingUrlParams gates any window.history updates?


582-590: All production <Booker> renderings are already wrapped by BookerStoreProvider

A repo-wide search confirms:

  • The two platform atoms (BookerPlatformWrapper and BookerWebWrapper) both wrap their children in <BookerStoreProvider>.
  • No other production code imports or renders <Booker> outside of those wrappers.
  • Test files in packages/features/bookings/Booker/__tests__ render <Booker> directly but use the test harness’s mockStore option to inject a fake store.

No further changes are needed.

Comment on lines +20 to 21
import { render, screen } from "./test-utils";

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add a test that verifies per-instance store isolation (core PR objective)

Good to see the custom render wrapper. However, there’s no test asserting that two Booker instances on the same page don’t share state. Please add one.

Here’s a starting point you can adapt to your test-utils and provider API:

@@
 describe("Booker", () => {
@@
   it("should render DryRunMessage when in dry run mode", () => {
@@
   });
 
+  it("isolates state across multiple Booker instances on the same page", () => {
+    const leftProps = {
+      ...defaultProps,
+      eventSlug: "left-event",
+      slots: {
+        ...defaultProps.slots,
+        setSelectedTimeslot: vi.fn(),
+      },
+    };
+    const rightProps = {
+      ...defaultProps,
+      eventSlug: "right-event",
+      slots: {
+        ...defaultProps.slots,
+        setSelectedTimeslot: vi.fn(),
+      },
+    };
+
+    // Render two instances; ensure each is wrapped with its own store/provider
+    // If your test-utils render() only supports a single mockStore,
+    // render them in two separate calls to simulate two widgets on one page.
+    const left = render(<Booker {...leftProps} />, {
+      mockStore: {
+        state: "booking",
+        selectedDate: "2024-01-01",
+        selectedTimeslot: "2024-01-01T10:00:00Z",
+        tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
+      },
+    });
+    const right = render(<Booker {...rightProps} />, {
+      mockStore: {
+        state: "booking",
+        selectedDate: "2024-01-02",
+        selectedTimeslot: "2024-01-02T10:00:00Z",
+        tentativeSelectedTimeslots: ["2024-01-02T10:00:00Z"],
+      },
+    });
+
+    // Act on the left instance (e.g., cancel)
+    left.getByRole("button", { name: /cancel/i }).click();
+
+    // Assert: left instance props updated, right instance untouched
+    expect((leftProps.slots.setSelectedTimeslot as any)).toHaveBeenCalledWith(null);
+    expect((rightProps.slots.setSelectedTimeslot as any)).not.toHaveBeenCalled();
+  });

If you prefer a single render with both siblings, ensure your test-utils can supply separate store providers per instance; I can help wire that up.

🤖 Prompt for AI Agents
packages/features/bookings/Booker/__tests__/Booker.test.tsx around lines 20-21:
the test file imports the custom render but lacks a test to assert per-instance
store isolation; add a new test that renders two Booker instances with distinct
stores (either by calling your custom render twice with independent
provider/store options or by rendering both siblings inside a wrapper that
supplies separate providers per child), perform an action that mutates the first
instance's state (e.g., select or add a booking), then assert the second
instance's UI/state remains unchanged; ensure the test uses your test-utils APIs
to create isolated stores and only references public UI queries (screen.getBy*,
etc.) to verify independence.

Comment on lines +74 to +76
state.setMonth = vi.fn((month: string | null) => {
state.month = month;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inconsistent mock behavior - only setMonth updates state

The mock implementation only provides a functional setter for setMonth, while all other setters remain as no-op mocks. This inconsistency could lead to confusing test behavior where some state updates work while others silently fail.

Consider either:

  1. Making all setters functional (preferred for integration tests), or
  2. Keeping all setters as mocks (for unit tests that verify function calls)

For a more complete mock that supports state updates:

-  state.setMonth = vi.fn((month: string | null) => {
-    state.month = month;
-  });
+  // Make all setters functional
+  state.setMonth = vi.fn((month: string | null) => {
+    state.month = month;
+  });
+  state.setVerifiedEmail = vi.fn((email: string | null) => {
+    state.verifiedEmail = email;
+  });
+  state.setState = vi.fn((newState: BookerState) => {
+    state.state = newState;
+  });
+  // ... continue for other setters
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
state.setMonth = vi.fn((month: string | null) => {
state.month = month;
});
// Make all setters functional
state.setMonth = vi.fn((month: string | null) => {
state.month = month;
});
state.setVerifiedEmail = vi.fn((email: string | null) => {
state.verifiedEmail = email;
});
state.setState = vi.fn((newState: BookerState) => {
state.state = newState;
});
// ... continue for other setters
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/__tests__/test-utils.tsx around lines 74 to
76, the mock only implements a functional setter for setMonth while other
setters are no-ops, causing inconsistent test behavior; either make all setters
consistent by turning setMonth into a no-op mock to match others, or (preferred)
make every setter mock functional so each setter updates its corresponding state
property (e.g., implement each setter as a vi.fn that assigns the incoming value
to the appropriate state field) so tests that rely on state changes behave
deterministically.

Comment on lines +67 to +112
useEffect(() => {
initializeStore({
username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
org,
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
});
}, [
initializeStore,
org,
username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
verifiedEmail,
durationConfig,
isInstantMeeting,
timezone,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider the impact of frequent re-initializations

The useEffect dependency array includes all initialization parameters, which means the store will be re-initialized whenever any prop changes. This could lead to unexpected state resets or performance issues.

Consider whether all these dependencies are necessary. Typically, initialization should only occur when key identifiers change (e.g., username, eventSlug, eventId). Props like timezone, verifiedEmail, or allowUpdatingUrlParams might be better handled through separate update methods rather than full re-initialization.


🏁 Script executed:

#!/bin/bash
# Check how the initialize method handles re-initialization in the store
ast-grep --pattern 'initialize: (data: StoreInitializeType) => {
  $$$
}'

Length of output: 75


I didn’t find an initializeStore implementation with the previous AST query. Let’s locate its definition and see how it handles repeated calls:


🏁 Script executed:

#!/bin/bash
# Search for where initializeStore is defined or imported
rg -n "initializeStore" --glob "*.ts*"

Length of output: 875


Let’s inspect the initialize implementation to see if it fully resets state or merges updates:


🏁 Script executed:

#!/bin/bash
# Show the initialize method in Booker store
sed -n '460,550p' packages/features/bookings/Booker/store.ts

Length of output: 1233


Let’s inspect the actual initialize action in the Booker store to see whether it fully resets state or just merges updates:


🏁 Script executed:

#!/bin/bash
# Locate and display the initialize action implementation
rg -n -C 5 "initialize:" packages/features/bookings/Booker/store.ts

Length of output: 883


Inspecting the full initialize implementation to see whether it resets or merges state:


🏁 Script executed:

#!/bin/bash
# Extract the body of the initialize action
sed -n '295,380p' packages/features/bookings/Booker/store.ts

Length of output: 2812


Align useEffect dependencies with the store’s guard logic

The initialize action in your Booker store only checks a specific subset of fields before early-returning (username, eventSlug, month, eventId, rescheduleUid, bookingUid, bookingData.responses.email, layout, timezone, rescheduledBy, teamMemberEmail, crmOwnerRecordType, crmAppSlug, crmRecordId). However, your useEffect dependency array also includes props—like org, isTeamEvent, durationConfig, isInstantMeeting, verifiedEmail, isPlatform, and allowUpdatingUrlParams—that the guard never compares. This means:

  • Changes to those extra props will trigger initialize calls that immediately return (no state update).
  • Conversely, if you do intend for some of those props to update the store, they’ll be ignored by the guard.

To resolve this mismatch, choose one of:

  • Narrow the dependency array in BookerStoreProvider.tsx to just the keys the guard actually compares.
  • Expand the guard in packages/features/bookings/Booker/store.ts (around lines 295–340) to include any additional props you do want to trigger a re-initialization.
  • Separate out specific update methods (e.g. setTimezone, setAllowUpdatingUrlParams) instead of feeding everything through initialize.

Places to update:

  • packages/features/bookings/Booker/BookerStoreProvider.tsx (lines 67–112) – adjust useEffect deps
  • packages/features/bookings/Booker/store.ts (initialize action at ~lines 295–340) – update guard or parameter handling
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/BookerStoreProvider.tsx around lines 67 to
112, the useEffect dependency array includes props that the store.initialize
guard does not check (org, isTeamEvent, durationConfig, isInstantMeeting,
verifiedEmail, isPlatform, allowUpdatingUrlParams), causing unnecessary
initialize calls or ignored updates; either narrow the dependency array to only
the keys the guard actually compares (username, eventSlug, month, eventId,
rescheduleUid, bookingUid/bookingData.responses.email, layout, timezone,
rescheduledBy, teamMemberEmail, crmOwnerRecordType, crmAppSlug, crmRecordId) or
update the initialize guard in packages/features/bookings/Booker/store.ts
(around lines 295–340) to compare and accept the additional props you want to
trigger re-initialization, or add specific setter actions (e.g., setTimezone,
setAllowUpdatingUrlParams) and call those from the provider instead of
initialize.

Comment on lines +196 to +218
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
}

// Setting month make sure small calendar in fullscreen layouts also updates.
// preventMonthSwitching is true in monthly view
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
const currentSelection = dayjs(get().selectedDate);
const newSelection = dayjs(selectedDate);
set({ selectedDate });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
updateQueryParam("date", selectedDate ?? "");
}
}
},
selectedDatesAndTimes: null,
setSelectedDatesAndTimes: (selectedDatesAndTimes) => {
set({ selectedDatesAndTimes });
},
addToSelectedDate: (days: number) => {
const currentSelection = dayjs(get().selectedDate);
let newSelection = currentSelection.add(days, "day");

// If newSelection is before the current date, set it to today
if (newSelection.isBefore(dayjs(), "day")) {
newSelection = dayjs();
}

const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
// Setting month make sure small calendar in fullscreen layouts also updates.
// preventMonthSwitching is true in monthly view
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential issue with null date handling in setSelectedDate.

When selectedDate is null (either initially or when unsetting), the dayjs instances created on lines 203-204 may be invalid, leading to unexpected behavior in the month comparison on line 212.

Consider adding null checks:

 setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
   // unset selected date
   if (!selectedDate) {
     removeQueryParam("date");
+    set({ selectedDate: null });
     return;
   }

-  const currentSelection = dayjs(get().selectedDate);
+  const currentSelectedDate = get().selectedDate;
+  const currentSelection = currentSelectedDate ? dayjs(currentSelectedDate) : dayjs();
   const newSelection = dayjs(selectedDate);
   set({ selectedDate });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
}
// Setting month make sure small calendar in fullscreen layouts also updates.
// preventMonthSwitching is true in monthly view
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
const currentSelection = dayjs(get().selectedDate);
const newSelection = dayjs(selectedDate);
set({ selectedDate });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
updateQueryParam("date", selectedDate ?? "");
}
}
},
selectedDatesAndTimes: null,
setSelectedDatesAndTimes: (selectedDatesAndTimes) => {
set({ selectedDatesAndTimes });
},
addToSelectedDate: (days: number) => {
const currentSelection = dayjs(get().selectedDate);
let newSelection = currentSelection.add(days, "day");
// If newSelection is before the current date, set it to today
if (newSelection.isBefore(dayjs(), "day")) {
newSelection = dayjs();
}
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
// Setting month make sure small calendar in fullscreen layouts also updates.
// preventMonthSwitching is true in monthly view
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
},
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
set({ selectedDate: null });
return;
}
const currentSelectedDate = get().selectedDate;
const currentSelection = currentSelectedDate
? dayjs(currentSelectedDate)
: dayjs();
const newSelection = dayjs(selectedDate);
set({ selectedDate });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("date", selectedDate);
}
// Setting month make sure small calendar in fullscreen layouts also updates.
// preventMonthSwitching is true in monthly view
if (
!preventMonthSwitching &&
newSelection.month() !== currentSelection.month()
) {
set({ month: newSelection.format("YYYY-MM") });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
},
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 196 to 218, guard
against get().selectedDate being null/undefined before creating currentSelection
and before comparing months: only construct dayjs(current) if get().selectedDate
is truthy (or treat it as an invalid sentinel), and perform the month-switching
logic only when both currentSelection and newSelection are valid dayjs objects;
if currentSelection is missing treat it as different so the month is updated.
Adjust the conditional around the month comparison and the
set/updateQueryParam("month", ...) calls to run only when newSelection is valid
and the comparison is safe.

Comment on lines +223 to +245
verifiedEmail: null,
setVerifiedEmail: (email: string | null) => {
set({ verifiedEmail: email });
},
month:
getQueryParam("month") ||
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
? dayjs(getQueryParam("date")).format("YYYY-MM")
: null) ||
dayjs().format("YYYY-MM"),
setMonth: (month: string | null) => {
if (!month) {
removeQueryParam("month");
return;
}
set({ month, selectedTimeslot: null });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", month ?? "");
}
get().setSelectedDate({ date: null });
},
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
setDayCount: (dayCount: number | null) => {
set({ dayCount });
},
isTeamEvent: false,
seatedEventData: {
seatsPerTimeSlot: undefined,
attendees: undefined,
bookingUid: undefined,
showAvailableSeatsCount: true,
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
}
},
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
timezone: getQueryParam("cal.tz") ?? null,
setTimezone: (timezone: string | null) => {
set({ timezone });
},
initialize: ({
username,
eventSlug,
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingUid = null,
bookingData = null,
layout,
isTeamEvent,
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");

if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}

if (
get().username === username &&
get().eventSlug === eventSlug &&
get().month === month &&
get().eventId === eventId &&
get().rescheduleUid === rescheduleUid &&
get().bookingUid === bookingUid &&
get().bookingData?.responses.email === bookingData?.responses.email &&
get().layout === layout &&
get().timezone === timezone &&
get().rescheduledBy === rescheduledBy &&
get().teamMemberEmail === teamMemberEmail &&
get().crmOwnerRecordType === crmOwnerRecordType &&
get().crmAppSlug === crmAppSlug &&
get().crmRecordId === crmRecordId
)
return;
set({
set({ selectedDate: newSelectionFormatted });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("date", newSelectionFormatted);
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle null selectedDate in addToSelectedDate.

The function doesn't handle the case where selectedDate might be null, which could cause issues when creating the dayjs instance.

Consider defaulting to today when selectedDate is null:

 addToSelectedDate: (days: number) => {
-  const currentSelection = dayjs(get().selectedDate);
+  const currentSelectedDate = get().selectedDate;
+  const currentSelection = currentSelectedDate ? dayjs(currentSelectedDate) : dayjs();
   let newSelection = currentSelection.add(days, "day");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
addToSelectedDate: (days: number) => {
const currentSelection = dayjs(get().selectedDate);
let newSelection = currentSelection.add(days, "day");
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
// If newSelection is before the current date, set it to today
if (newSelection.isBefore(dayjs(), "day")) {
newSelection = dayjs();
}
}
set({ selectedDate: newSelectionFormatted });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("date", newSelectionFormatted);
}
},
username: null,
eventSlug: null,
eventId: null,
rescheduledBy: null,
verifiedEmail: null,
setVerifiedEmail: (email: string | null) => {
set({ verifiedEmail: email });
},
month:
getQueryParam("month") ||
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
? dayjs(getQueryParam("date")).format("YYYY-MM")
: null) ||
dayjs().format("YYYY-MM"),
setMonth: (month: string | null) => {
if (!month) {
removeQueryParam("month");
return;
}
set({ month, selectedTimeslot: null });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", month ?? "");
}
get().setSelectedDate({ date: null });
},
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
setDayCount: (dayCount: number | null) => {
set({ dayCount });
},
isTeamEvent: false,
seatedEventData: {
seatsPerTimeSlot: undefined,
attendees: undefined,
bookingUid: undefined,
showAvailableSeatsCount: true,
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
}
},
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
timezone: getQueryParam("cal.tz") ?? null,
setTimezone: (timezone: string | null) => {
set({ timezone });
},
initialize: ({
username,
eventSlug,
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingUid = null,
bookingData = null,
layout,
isTeamEvent,
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
if (
get().username === username &&
get().eventSlug === eventSlug &&
get().month === month &&
get().eventId === eventId &&
get().rescheduleUid === rescheduleUid &&
get().bookingUid === bookingUid &&
get().bookingData?.responses.email === bookingData?.responses.email &&
get().layout === layout &&
get().timezone === timezone &&
get().rescheduledBy === rescheduledBy &&
get().teamMemberEmail === teamMemberEmail &&
get().crmOwnerRecordType === crmOwnerRecordType &&
get().crmAppSlug === crmAppSlug &&
get().crmRecordId === crmRecordId
)
return;
set({
set({ selectedDate: newSelectionFormatted });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("date", newSelectionFormatted);
}
},
addToSelectedDate: (days: number) => {
const currentSelectedDate = get().selectedDate;
const currentSelection = currentSelectedDate
? dayjs(currentSelectedDate)
: dayjs();
let newSelection = currentSelection.add(days, "day");
// If newSelection is before the current date, set it to today
if (newSelection.isBefore(dayjs(), "day")) {
newSelection = dayjs();
}
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
set({ selectedDate: newSelectionFormatted });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("date", newSelectionFormatted);
}
},
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 223 to 245, add
handling for a null or undefined selectedDate before constructing the dayjs
instance: if get().selectedDate is null/undefined, treat currentSelection as
today (dayjs()), then proceed with computing newSelection, month and date
updates and URL param updates as before; ensure you use the resolved
currentSelection for month comparison and formatting so no dayjs is constructed
with a null value.

@@ -4,7 +4,7 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify all DatePicker usages are within BookerStoreProvider

This file now requires BookerStoreProvider at runtime. Please verify all consumers import/use this component within a provider to avoid runtime errors.

Run this script to list all imports of calendars/DatePicker and quickly spot non-Booker contexts:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Files importing @calcom/features/calendars/DatePicker:"
rg -n $'@calcom/features/calendars/DatePicker' | cut -d: -f1 | sort -u

echo
echo "Heuristic check for files that render <DatePicker ...> without nearby BookerStoreProvider:"
# List lines with DatePicker JSX; manual inspection recommended for provider presence.
rg -n -A 3 -B 3 $'<DatePicker[\\s>]' | sed -n '1,200p'

Length of output: 6298


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== packages/features/embed/Embed.tsx ==="
rg -n 'import.*DatePicker' -C3 packages/features/embed/Embed.tsx || true
rg -n '<DatePicker' -C3 packages/features/embed/Embed.tsx || true
rg -n 'BookerStoreProvider' -C3 packages/features/embed/Embed.tsx || true

echo
echo "=== packages/features/bookings/Booker/components/DatePicker.tsx ==="
rg -n 'import' -C5 packages/features/bookings/Booker/components/DatePicker.tsx || true
rg -n '<DatePicker' -C5 packages/features/bookings/Booker/components/DatePicker.tsx || true
rg -n 'BookerStoreProvider' -C5 packages/features/bookings/Booker/components/DatePicker.tsx || true

echo
echo "=== packages/features/bookings/Booker/components/DatePicker.test.tsx ==="
rg -n '<DatePicker' -C3 packages/features/bookings/Booker/components/DatePicker.test.tsx || true
rg -n 'BookerStoreProvider' -C3 packages/features/bookings/Booker/components/DatePicker.test.tsx || true

Length of output: 4935


Wrap <DatePicker> in Embed.tsx with BookerStoreProvider

packages/features/embed/Embed.tsx currently imports and renders the core <DatePicker> outside of any provider. Since @calcom/features/calendars/DatePicker now calls useBookerStoreContext, it must be rendered within <BookerStoreProvider> to avoid “useContext” runtime errors.

Action items:

  • At the top of packages/features/embed/Embed.tsx, add:
    import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
  • Wrap the existing <DatePicker> JSX block:
       <div className="text-default text-sm">{t("select_date")}</div>
    -  <DatePicker
    +  <BookerStoreProvider>
    +    <DatePicker
         isLoading={schedule.isPending}
         onChange={(date: Dayjs | null) => {
           setSelectedDate(date === null ? null : date.format("YYYY-MM-DD"));
    +  </BookerStoreProvider>

Also audit any other direct <DatePicker> usages (including tests) to ensure they either mock the context or are wrapped in BookerStoreProvider.

🤖 Prompt for AI Agents
In packages/features/embed/Embed.tsx at the top of the file and in
packages/features/calendars/DatePicker.tsx around line 7, the DatePicker is
being rendered outside the BookerStoreProvider; import BookerStoreProvider from
"@calcom/features/bookings/Booker/BookerStoreProvider" at the top of Embed.tsx
and wrap the existing <DatePicker> JSX with
<BookerStoreProvider>...</BookerStoreProvider>; then audit other direct
DatePicker usages (components and tests) and either wrap them in
BookerStoreProvider or mock useBookerStoreContext in tests.

Comment on lines +181 to +196
useInitializeBookerStoreContext({
...props,
teamMemberEmail,
crmAppSlug,
crmOwnerRecordType,
crmRecordId: props.crmRecordId,
eventId: event?.data?.id,
rescheduleUid: props.rescheduleUid ?? null,
bookingUid: props.bookingUid ?? null,
layout: layout,
org: props.entity?.orgSlug,
username,
bookingData,
isPlatform: true,
allowUpdatingUrlParams,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Initialize only the context store; remove legacy initializer

You’re initializing both the old global store (useInitializeBookerStore at Line 165) and the new context store here. Initializing the legacy singleton undermines per-instance isolation and can leak state across widgets.

Please remove the legacy initializer and rely solely on useInitializeBookerStoreContext. Suggested diff:

-  useInitializeBookerStore({
-    ...props,
-    teamMemberEmail,
-    crmAppSlug,
-    crmOwnerRecordType,
-    crmRecordId: props.crmRecordId,
-    eventId: event?.data?.id,
-    rescheduleUid: props.rescheduleUid ?? null,
-    bookingUid: props.bookingUid ?? null,
-    layout: layout,
-    org: props.entity?.orgSlug,
-    username,
-    bookingData,
-    isPlatform: true,
-    allowUpdatingUrlParams,
-  });

Also remove the unused import:

- import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
+ import { /* remove legacy store imports */ } from "@calcom/features/bookings/Booker/store";

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 165 and
181-196, remove the legacy global initializer useInitializeBookerStore (the call
at ~line 165) so only useInitializeBookerStoreContext is invoked, and delete the
now-unused import for the legacy hook; ensure per-instance isolation by keeping
the existing useInitializeBookerStoreContext call and removing the old
initializer and its import from the top of the file.

💡 Verification agent

🧩 Analysis chain

Repo-wide verification: detect lingering usage of the legacy singleton store

To ensure multi-widget isolation, we should remove or quarantine useBookerStore usage outside the provider. This script highlights any remaining direct imports/usages of the legacy store in Booker paths.

Also applies to: 111-128, 17-17


🏁 Script executed:

#!/bin/bash
# 1) Find legacy store imports/usages
rg -n --no-heading $'@calcom/features/bookings/Booker/store|useBookerStore\\(|useInitializeBookerStore\\('

# 2) Specifically flag subscribe/getState calls on the legacy store
rg -n --no-heading $'useBookerStore\\.subscribe|useBookerStore\\.getState'

# 3) Ensure provider wrapping is present wherever Booker is rendered
rg -n --no-heading $'BookerStoreProvider'

Length of output: 12859


Action Required: Quarantine all legacy useBookerStore imports and calls outside of BookerStoreProvider

Our scan shows numerous direct imports and usages of the singleton useBookerStore (and even .subscribe/.getState) scattered across both feature and platform code. To guarantee true multi‐widget isolation, every component must instead consume the store via context—i.e. within a BookerStoreProvider using useBookerStoreContext (and only initialize via useInitializeBookerStoreContext in your wrapper).

Key locations needing migration:

  • packages/features/schedules/lib/use-schedule/useSchedule.ts
  • packages/features/embed/Embed.tsx
  • All files under packages/features/bookings/components/** and their hooks
  • packages/features/bookings/Booker/** (components, hooks, utils)
  • packages/platform/atoms/booker/BookerPlatformWrapper.tsx
  • packages/platform/atoms/booker/BookerWebWrapper.tsx

Next steps:

  • Remove all direct import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"
  • Replace with useBookerStoreContext and useInitializeBookerStoreContext inside a BookerStoreProvider wrapper
  • Eliminate any calls to useBookerStore.subscribe or useBookerStore.getState

This ensures each widget instance maintains its own isolated store without leaking or sharing global state.

🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 181 to
196, quarantine and remove any direct uses or imports of the singleton
useBookerStore (including .subscribe and .getState) outside of a
BookerStoreProvider: replace imports of useBookerStore /
useInitializeBookerStore with useBookerStoreContext and
useInitializeBookerStoreContext, ensure this wrapper mounts a
BookerStoreProvider and calls useInitializeBookerStoreContext here (only), and
update all nested components to consume the store via useBookerStoreContext (not
the singleton) so no component imports or calls the global store APIs directly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🔭 Outside diff range comments (3)
packages/features/bookings/Booker/components/Header.tsx (2)

113-120: Localize aria-labels per guidelines

Avoid hardcoded strings in TSX. Wrap aria-labels with t().

-          <Button
+          <Button
             className="group rtl:ml-1 rtl:rotate-180"
             variant="icon"
             color="minimal"
             StartIcon="chevron-left"
-            aria-label="Previous Day"
+            aria-label={t("previous_day")}
             onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? -nextSlots : -extraDays)}
           />
           <Button
             className="group rtl:mr-1 rtl:rotate-180"
             variant="icon"
             color="minimal"
             StartIcon="chevron-right"
-            aria-label="Next Day"
+            aria-label={t("next_day")}
             onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? nextSlots : extraDays)}
           />

Also applies to: 121-128


186-213: Bug: "${t(...)}" renders literally; use {t(...)} in JSX

Screen-reader labels currently include literal “${…}”. Replace with JSX expression to localize correctly.

-            <span className="sr-only">${t("switch_monthly")}</span>
+            <span className="sr-only">{t("switch_monthly")}</span>
@@
-            <span className="sr-only">${t("switch_weekly")}</span>
+            <span className="sr-only">{t("switch_weekly")}</span>
@@
-            <span className="sr-only">${t("switch_columnview")}</span>
+            <span className="sr-only">{t("switch_columnview")}</span>
packages/platform/atoms/hooks/useSlots.ts (1)

88-102: Avoid stale reservation params; simplify dependency by using selectedTimeslot directly

The effect depends on timeslot only, but handleReserveSlot also captures selectedDuration and seatedEventData.bookingUid. If either changes, reservations may lag until the next interval or timeslot change.

Recommend:

  • Drop the extra timeslot variable and use selectedTimeslot directly.
  • Include selectedDuration and seatedEventData?.bookingUid in the dependency array to keep reservations in sync.
-  const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
+  // use the already-selected `selectedTimeslot` directly; no extra selector needed

@@
-  }, [event?.data?.id, timeslot]);
+  }, [event?.data?.id, selectedTimeslot, selectedDuration, seatedEventData?.bookingUid]);
🧹 Nitpick comments (8)
.changeset/fresh-rivers-glow.md (1)

5-5: Grammar nitpick in changeset

Fix minor typo for clarity.

-Fixes an issues for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others.
+Fixes an issue in the Booker atom where placing multiple widgets on the same page caused changes in one widget to be reflected in the others.
packages/features/bookings/Booker/components/DatePicker.tsx (1)

105-106: Prefer enum over string literals for layout checks

For consistency with other files (e.g., Header.tsx), consider using BookerLayouts enum instead of string literals for layout comparisons.

-const isCompact = layout !== "month_view" && layout !== "mobile";
+import { BookerLayouts } from "@calcom/prisma/zod-utils";
+const isCompact = layout !== BookerLayouts.MONTH_VIEW && layout !== "mobile";
packages/platform/atoms/booker/BookerWebWrapper.tsx (1)

7-7: Remove redundant React import

The explicit React import is unnecessary in modern Next.js applications since React is automatically available in JSX files.

-import React from "react";
packages/features/bookings/Booker/components/DatePicker.test.tsx (1)

1-1: Remove redundant React import

The explicit React import is unnecessary in modern test files since React is automatically available when using JSX.

-import React from "react";
packages/features/bookings/Booker/__tests__/Booker.test.tsx (1)

13-13: Remove redundant React import

The explicit React import is unnecessary in test files since React is automatically available when using JSX.

-import React from "react";
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)

37-113: Two initializer hooks now exist; consider consolidating

There’s duplication between useInitializeBookerStoreContext here and useInitializeBookerStore in store.ts. Prefer a single, context-first initializer to reduce drift and confusion. You can re-export the context version and deprecate the legacy one.

packages/features/bookings/Booker/store.ts (1)

451-452: Legacy global store export retained

Keeping useBookerStore for backward compatibility is fine short-term. Consider adding a deprecation JSDoc to steer new code to the Provider pattern.

packages/platform/atoms/booker/BookerPlatformWrapper.tsx (1)

197-199: Optimize selector to avoid unnecessary rerenders

You select both dayCount and setDayCount but only use dayCount. Selecting functions inside the tuple prevents shallow equality from short-circuiting effectively.

-  const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
+  const dayCount = useBookerStoreContext((state) => state.dayCount);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebeb008 and 4a259b3.

📒 Files selected for processing (21)
  • .changeset/fresh-rivers-glow.md (1 hunks)
  • packages/features/bookings/Booker/Booker.tsx (7 hunks)
  • packages/features/bookings/Booker/BookerStoreProvider.tsx (1 hunks)
  • packages/features/bookings/Booker/__tests__/Booker.test.tsx (5 hunks)
  • packages/features/bookings/Booker/__tests__/test-utils.tsx (1 hunks)
  • packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (5 hunks)
  • packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (4 hunks)
  • packages/features/bookings/Booker/components/DatePicker.test.tsx (5 hunks)
  • packages/features/bookings/Booker/components/DatePicker.tsx (2 hunks)
  • packages/features/bookings/Booker/components/EventMeta.tsx (3 hunks)
  • packages/features/bookings/Booker/components/Header.tsx (2 hunks)
  • packages/features/bookings/Booker/components/hooks/useBookings.ts (2 hunks)
  • packages/features/bookings/Booker/components/hooks/useSlots.ts (2 hunks)
  • packages/features/bookings/Booker/store.ts (2 hunks)
  • packages/features/bookings/Booker/utils/event.ts (3 hunks)
  • packages/features/calendars/DatePicker.tsx (3 hunks)
  • packages/features/calendars/__tests__/DatePicker.test.tsx (7 hunks)
  • packages/platform/atoms/booker/BookerPlatformWrapper.tsx (5 hunks)
  • packages/platform/atoms/booker/BookerWebWrapper.tsx (3 hunks)
  • packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (2 hunks)
  • packages/platform/atoms/hooks/useSlots.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.ts
  • packages/features/bookings/Booker/utils/event.ts
  • packages/platform/atoms/hooks/useSlots.ts
  • packages/features/bookings/Booker/components/hooks/useSlots.ts
  • packages/features/bookings/Booker/store.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.ts
  • packages/features/calendars/__tests__/DatePicker.test.tsx
  • packages/features/bookings/Booker/BookerStoreProvider.tsx
  • packages/features/bookings/Booker/components/DatePicker.tsx
  • packages/features/bookings/Booker/utils/event.ts
  • packages/features/bookings/Booker/__tests__/test-utils.tsx
  • packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
  • packages/platform/atoms/booker/BookerWebWrapper.tsx
  • packages/platform/atoms/hooks/useSlots.ts
  • packages/features/bookings/Booker/components/DatePicker.test.tsx
  • packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
  • packages/features/calendars/DatePicker.tsx
  • packages/features/bookings/Booker/components/hooks/useSlots.ts
  • packages/features/bookings/Booker/Booker.tsx
  • packages/features/bookings/Booker/components/EventMeta.tsx
  • packages/features/bookings/Booker/__tests__/Booker.test.tsx
  • packages/features/bookings/Booker/components/Header.tsx
  • packages/platform/atoms/booker/BookerPlatformWrapper.tsx
  • packages/features/bookings/Booker/store.ts
**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/calendars/__tests__/DatePicker.test.tsx
  • packages/features/bookings/Booker/BookerStoreProvider.tsx
  • packages/features/bookings/Booker/components/DatePicker.tsx
  • packages/features/bookings/Booker/__tests__/test-utils.tsx
  • packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
  • packages/platform/atoms/booker/BookerWebWrapper.tsx
  • packages/features/bookings/Booker/components/DatePicker.test.tsx
  • packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
  • packages/features/calendars/DatePicker.tsx
  • packages/features/bookings/Booker/Booker.tsx
  • packages/features/bookings/Booker/components/EventMeta.tsx
  • packages/features/bookings/Booker/__tests__/Booker.test.tsx
  • packages/features/bookings/Booker/components/Header.tsx
  • packages/platform/atoms/booker/BookerPlatformWrapper.tsx
🧬 Code Graph Analysis (18)
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/calendars/__tests__/DatePicker.test.tsx (5)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • BookerStoreProvider (15-22)
packages/platform/atoms/src/components/ui/tooltip.tsx (1)
  • TooltipProvider (14-14)
packages/features/calendars/DatePicker.tsx (1)
  • DatePicker (493-493)
packages/features/bookings/Booker/components/DatePicker.tsx (1)
  • DatePicker (54-153)
packages/platform/libraries/index.ts (1)
  • PeriodType (30-30)
packages/features/bookings/Booker/components/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/utils/event.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/__tests__/test-utils.tsx (3)
packages/features/bookings/Booker/store.ts (1)
  • BookerStore (51-174)
packages/embeds/embed-core/src/types.ts (1)
  • BookerLayouts (6-6)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • BookerStoreContext (9-9)
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (2)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/embeds/embed-core/src/types.ts (1)
  • BookerLayouts (6-6)
packages/platform/atoms/booker/BookerWebWrapper.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • BookerStoreProvider (15-22)
packages/platform/atoms/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/calendars/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/Booker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/components/EventMeta.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (1)
packages/features/bookings/Booker/Booker.tsx (1)
  • Booker (593-599)
packages/features/bookings/Booker/components/Header.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
  • useBookerStoreContext (24-35)
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (1)
packages/platform/atoms/booker/types.ts (2)
  • BookerPlatformWrapperAtomPropsForIndividual (92-96)
  • BookerPlatformWrapperAtomPropsForTeam (98-103)
packages/features/bookings/Booker/store.ts (3)
packages/features/bookings/Booker/types.ts (2)
  • BookerState (154-154)
  • BookerLayout (155-155)
packages/embeds/embed-core/src/types.ts (1)
  • BookerLayouts (6-6)
packages/lib/constants.ts (1)
  • BOOKER_NUMBER_OF_DAYS_TO_LOAD (170-173)
🔇 Additional comments (39)
packages/features/bookings/Booker/components/DatePicker.tsx (1)

5-5: Migration Incomplete: Direct useBookerStore References Detected

The migration to useBookerStoreContext is not yet complete—there are numerous direct useBookerStore imports and calls still in the codebase. Please replace these with useBookerStoreContext (and ensure the corresponding provider wraps each entry point). Key locations needing attention:

• packages/platform/atoms/booker/BookerWebWrapper.tsx
• packages/platform/atoms/booker/BookerPlatformWrapper.tsx
• packages/features/schedules/lib/use-schedule/useSchedule.ts
• packages/features/bookings/components/AvailableTimes.tsx
• packages/features/embed/Embed.tsx
• All remaining imports of useBookerStore under packages/features/bookings/

Example replacement in BookerWebWrapper.tsx:

- import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
+ import { useBookerStoreContext, useInitializeBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";

Continue this pattern project-wide to remove all direct store hooks and ensure every component consuming the store is wrapped by BookerStoreProvider.

Likely an incorrect or invalid review comment.

packages/features/bookings/Booker/utils/event.ts (1)

3-3: LGTM: context-based store migration is correct

Selectors and shallow equality usage are appropriate. This aligns with per-instance store isolation.

Also applies to: 22-25, 92-95

packages/features/bookings/Booker/components/Header.tsx (1)

7-7: LGTM: store access migrated to context

Header now correctly uses useBookerStoreContext for both reads and actions.

Also applies to: 39-43

packages/features/calendars/__tests__/DatePicker.test.tsx (1)

7-7: Tests correctly wrap DatePicker with BookerStoreProvider

Good adjustment to match useBookerStoreContext usage; keeps tests aligned with runtime usage.

Also applies to: 20-36, 46-51, 61-66, 95-113, 139-157, 179-197

packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)

4-4: LGTM: context-based migration preserves isolation

All booker state reads/actions are via useBookerStoreContext, removing global coupling.

Also applies to: 50-66

packages/platform/atoms/booker/BookerWebWrapper.tsx (3)

36-268: LGTM: Clean extraction of internal component

The extraction of the previous BookerWebWrapper logic into BookerPlatformWrapperComponent is well-implemented and maintains all existing functionality while enabling the context-based store pattern.


188-191: Good: Persistent overlay calendar preference

The localStorage implementation correctly persists the overlay calendar default state across page reloads, improving user experience by remembering their preference.


270-276: LGTM: Context provider wrapper maintains backward compatibility

The new BookerWebWrapper correctly wraps the internal component with BookerStoreProvider, enabling per-instance store isolation while preserving the original public API.

packages/features/bookings/Booker/components/DatePicker.test.tsx (2)

64-64: LGTM: Fixed Day.js parameter types

The correction from string parameters ("1") to number parameters (1) in dayjs().add() calls is proper usage and aligns with Day.js API expectations.

Also applies to: 106-106, 114-114


76-81: Good: Context-based test setup

The new mockStore configuration approach enables per-test state initialization through the BookerStoreProvider context, providing better test isolation and more explicit test setup.

Also applies to: 104-108

packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (3)

46-46: LGTM: Explicit timeslot prop improves component clarity

Making the selected timeslot an explicit prop rather than internal state makes the component's dependencies clearer and aligns with the centralized timeslot management pattern.

Also applies to: 66-66


75-79: Good: Context-based store access migration

The migration from useBookerStore to useBookerStoreContext maintains the same selectors while enabling proper context-based state management and store isolation per Booker instance.


94-103: Appropriate fallback for missing timeslot

The EmptyScreen component provides clear user feedback when no timeslot is selected, with actionable guidance to return and select a time. This improves the user experience for edge cases.

packages/features/bookings/Booker/__tests__/Booker.test.tsx (3)

23-23: Good: Proper type casting for mock

The type casting as any for the framer-motion mock is appropriate since we're dealing with a dynamic import mock that may not have complete type information.


177-179: Excellent: Per-test store configuration

The new mockStore approach provides better test isolation by configuring initial state per-test rather than mutating a shared global state. This makes tests more predictable and easier to debug.

Also applies to: 196-203, 221-222, 242-244


169-169: LGTM: Updated rollout constant

The update to PUBLIC_QUICK_AVAILABILITY_ROLLOUT from a percentage to "100" aligns with the new expected behavior in the system.

packages/features/bookings/Booker/components/hooks/useSlots.ts (1)

5-5: LGTM: Clean migration to context-based store access

The migration from useBookerStore to useBookerStoreContext maintains identical selectors and shallow comparison patterns while enabling proper context-based state management. The hook's public interface and behavior remain unchanged.

Also applies to: 81-81, 84-92

packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (3)

52-52: Good: Callback pattern for centralized timeslot management

The new onAvailableTimeSlotSelect callback prop enables centralized timeslot selection management while maintaining clear component boundaries and responsibilities.

Also applies to: 76-76


79-79: LGTM: Context-based store access migration

The migration from useBookerStore to useBookerStoreContext maintains the same selectors while enabling proper context-based state management consistent with the overall architectural changes.

Also applies to: 81-81, 83-83, 86-89


151-151: Proper callback usage and dependencies

Replacing the direct setSelectedTimeslot call with the onAvailableTimeSlotSelect callback correctly delegates timeslot management to the parent component. The callback is appropriately included in the useCallback dependencies array.

Also applies to: 167-167

packages/features/bookings/Booker/components/EventMeta.tsx (4)

57-57: LGTM: Explicit selectedTimeslot prop improves component clarity

Making the selected timeslot an explicit prop rather than internal state makes the component's dependencies clearer and enables better control over when the time selection is displayed.

Also applies to: 95-95


100-108: Good: Context-based store access migration

The migration from useBookerStore to useBookerStoreContext maintains the same selectors and shallow comparison patterns while enabling proper context-based state management.


206-216: Appropriate conditional rendering for timeslot

The conditional rendering based on selectedTimeslot prop ensures the calendar block only appears when a timeslot is actually selected, improving the user interface clarity.


248-250: Good: Dual timezone state management

The timezone onChange handler correctly updates both the local timePreferences state and the Booker store context, maintaining consistency across the application's timezone handling.

packages/features/calendars/DatePicker.tsx (1)

7-7: LGTM: Straightforward context-based store migration

The migration from useBookerStore to useBookerStoreContext maintains identical selectors and behavior while enabling proper context-based state management. The calendar functionality remains unchanged.

Also applies to: 227-227, 395-395

packages/platform/atoms/hooks/useSlots.ts (3)

39-43: Good move to per-instance store via context

Selectors + shallow look correct and avoid re-renders. This removes the shared global-store coupling.


55-55: Selector usage LGTM

Accessing seatedEventData from the context is consistent with the new store shape.


5-6: All useSlots invocations are already wrapped in BookerStoreProvider

I’ve verified that both consumers of useSlots live inside the provider:

  • In packages/platform/atoms/booker/BookerPlatformWrapper.tsx, the hook call at line 386 is within the <BookerStoreProvider> wrapper (lines 586–589).
  • In packages/platform/atoms/booker/BookerWebWrapper.tsx, the hook call at line 124 is inside the <BookerStoreProvider> wrapper (lines 272–274).

No further changes needed.

packages/features/bookings/Booker/Booker.tsx (3)

87-94: Context migration looks good

Reading [state, setState], selectedDate, and setSelectedDate from useBookerStoreContext is correct and isolates Booker instances per provider.


107-115: LGTM on additional selectors

Using context for [seatedEventData, setSeatedEventData] and [dayCount, setDayCount] aligns the component with the new store architecture.


194-197: Timeslot selection flow is clear; watch out for cross-instance URL coupling

Hooking AvailableTimeSlots to setSelectedTimeslot and threading selectedTimeslot down to consumers is clean.

However, setSelectedTimeslot updates the shared URL query param (slot) in the store. With multiple Booker instances on the same page and allowUpdatingUrlParams=true, changing one could inadvertently affect others via URL state.

Consider:

  • Defaulting allowUpdatingUrlParams to false when multiple providers are detected, or
  • Namespacing query params per instance (e.g., slot_<instanceId>)

Can you confirm multi-widget pages set allowUpdatingUrlParams=false for all instances in production scenarios, or adopt namespacing?

Also applies to: 492-512

packages/features/bookings/Booker/BookerStoreProvider.tsx (2)

15-22: Per-provider singleton store is correct

Using useRef to create one store instance per provider ensures isolation across multiple widgets on the page.


24-35: Selector hook with guard is solid

Throwing outside the provider prevents silent misuse. useStore(context, selector, equalityFn) keeps components efficient.

packages/features/bookings/Booker/store.ts (3)

271-275: Default dayCount initialization LGTM

Using BOOKER_NUMBER_OF_DAYS_TO_LOAD to conditionally seed dayCount is fine and keeps behavior configurable.


361-385: Initialization logic is robust; nice handling for reschedule + duration

Duration sync, clearing slot on reschedule, and URL updates look correct.


431-435: Cross-instance coupling via URL params

Even with isolated stores, updating query params (date, month, slot, duration, bookingUid) is a shared global channel. On pages with multiple Booker instances and allowUpdatingUrlParams=true, one instance may influence another through URL-driven effects.

Mitigations:

  • Default allowUpdatingUrlParams to false when multiple providers are mounted.
  • Namespace params per instance (e.g., slot_<instanceId>), with instanceId provided via props.
  • Gate reading of query params behind isPlatform && allowUpdatingUrlParams as you already do for writes.

Do you want me to draft a minimal namespacing design (provider-level instanceId + helpers), or should we just flip allowUpdatingUrlParams to false by default for multi-instance scenarios?

Also applies to: 295-406

packages/platform/atoms/booker/BookerPlatformWrapper.tsx (3)

9-13: Context provider imports look correct for per-instance isolation

Good move importing BookerStoreProvider and the context hooks; aligns with the goal of isolating each widget’s state.


200-202: LGTM: reading month and eventSlug from context

This aligns with the per-instance store usage.


582-590: LGTM: Provider at the public wrapper boundary ensures store isolation per widget

Wrapping BookerPlatformWrapper with BookerStoreProvider correctly guarantees a separate store per widget instance. Verify that other wrappers (e.g., BookerWebWrapper) don’t double-wrap unintentionally.

To double-check isolation manually:

  • Render two BookerPlatformWrapper instances side by side with allowUpdatingUrlParams={false}.
  • Change selected date/duration in one; confirm the other stays unchanged.

Comment on lines +18 to +91
const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
let state: BookerStore = {
username: null,
eventSlug: null,
eventId: null,
verifiedEmail: null,
setVerifiedEmail: vi.fn(),
month: dayjs().format("YYYY-MM"),
setMonth: vi.fn(),
state: "loading",
setState: vi.fn(),
layout: BookerLayouts.MONTH_VIEW,
setLayout: vi.fn(),
selectedDate: null,
setSelectedDate: vi.fn(),
addToSelectedDate: vi.fn(),
selectedDatesAndTimes: null,
setSelectedDatesAndTimes: vi.fn(),
durationConfig: null,
selectedDuration: null,
setSelectedDuration: vi.fn(),
selectedTimeslot: null,
setSelectedTimeslot: vi.fn(),
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: vi.fn(),
recurringEventCount: null,
setRecurringEventCount: vi.fn(),
occurenceCount: null,
setOccurenceCount: vi.fn(),
dayCount: null,
setDayCount: vi.fn(),
rescheduleUid: null,
rescheduledBy: null,
bookingUid: null,
bookingData: null,
setBookingData: vi.fn(),
initialize: vi.fn(),
formValues: {},
setFormValues: vi.fn(),
isTeamEvent: false,
seatedEventData: {},
setSeatedEventData: vi.fn(),
isInstantMeeting: false,
org: null,
setOrg: vi.fn(),
timezone: null,
setTimezone: vi.fn(),
teamMemberEmail: null,
crmOwnerRecordType: null,
crmAppSlug: null,
crmRecordId: null,
isPlatform: false,
allowUpdatingUrlParams: true,
...initialState,
};

state.setMonth = vi.fn((month: string | null) => {
state.month = month;
});

return {
getState: () => state,
setState: vi.fn((updater) => {
if (typeof updater === "function") {
const newState = updater(state);
state = { ...state, ...newState };
} else {
state = { ...state, ...updater };
}
}),
subscribe: vi.fn(),
destroy: vi.fn(),
} as unknown as StoreApi<BookerStore>;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Mock StoreApi must implement subscribe and notify; setters should update state

Current mock store won’t work with useStore:

  • subscribe returns undefined; useStore expects an unsubscribe function.
  • setState doesn’t notify subscribers.
  • Most setters are vi.fn() no-ops, so UI won’t re-render in tests.

Implement a minimal reactive store:

@@
-const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
-  let state: BookerStore = {
+const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
+  type Listener = (next: BookerStore, prev: BookerStore) => void;
+  const listeners = new Set<Listener>();
+  let state: BookerStore = {
@@
-    layout: BookerLayouts.MONTH_VIEW,
+    layout: BookerLayouts.MONTH_VIEW,
     setLayout: vi.fn(),
@@
-    setSelectedDate: vi.fn(),
+    setSelectedDate: vi.fn((params: { date: string | null }) => {
+      const next = { ...state, selectedDate: params.date ?? null };
+      const prev = state;
+      state = next;
+      listeners.forEach((l) => l(next, prev));
+    }),
@@
-    selectedDuration: null,
-    setSelectedDuration: vi.fn(),
+    selectedDuration: null,
+    setSelectedDuration: vi.fn((duration: number | null) => {
+      const next = { ...state, selectedDuration: duration };
+      const prev = state;
+      state = next;
+      listeners.forEach((l) => l(next, prev));
+    }),
@@
-    selectedTimeslot: null,
-    setSelectedTimeslot: vi.fn(),
+    selectedTimeslot: null,
+    setSelectedTimeslot: vi.fn((slot: string | null) => {
+      const next = { ...state, selectedTimeslot: slot };
+      const prev = state;
+      state = next;
+      listeners.forEach((l) => l(next, prev));
+    }),
@@
-    seatedEventData: {},
+    seatedEventData: {
+      seatsPerTimeSlot: undefined,
+      attendees: undefined,
+      bookingUid: undefined,
+      showAvailableSeatsCount: true,
+    },
     setSeatedEventData: vi.fn(),
@@
-  state.setMonth = vi.fn((month: string | null) => {
-    state.month = month;
-  });
+  state.setMonth = vi.fn((month: string | null) => {
+    const next = { ...state, month };
+    const prev = state;
+    state = next;
+    listeners.forEach((l) => l(next, prev));
+  });
@@
-  return {
-    getState: () => state,
-    setState: vi.fn((updater) => {
-      if (typeof updater === "function") {
-        const newState = updater(state);
-        state = { ...state, ...newState };
-      } else {
-        state = { ...state, ...updater };
-      }
-    }),
-    subscribe: vi.fn(),
-    destroy: vi.fn(),
-  } as unknown as StoreApi<BookerStore>;
+  return {
+    getState: () => state,
+    setState: vi.fn((updater: Partial<BookerStore> | ((s: BookerStore) => Partial<BookerStore>)) => {
+      const partial = typeof updater === "function" ? updater(state) : updater;
+      const next = { ...state, ...partial };
+      const prev = state;
+      state = next;
+      listeners.forEach((l) => l(next, prev));
+    }),
+    subscribe: vi.fn((listener: Listener) => {
+      listeners.add(listener);
+      return () => {
+        listeners.delete(listener);
+      };
+    }),
+    destroy: vi.fn(),
+  } as unknown as StoreApi<BookerStore>;

Optionally implement similar updates for: setDayCount, setFormValues, setBookingData, setState, setLayout—so tests relying on them re-render deterministically.

🤖 Prompt for AI Agents
In packages/features/bookings/Booker/__tests__/test-utils.tsx around lines 18 to
91, the mock StoreApi doesn't implement subscription notification and most
setter fns are no-ops so useStore won't re-render in tests; implement a minimal
reactive store by keeping an internal array of subscribers, make subscribe push
a listener and return an unsubscribe function, update setState to compute the
new state (handling updater functions), assign it to state, then call all
subscribers with the new state, and replace vi.fn() no-op setters (at least
setMonth, setDayCount, setFormValues, setBookingData, setLayout and any other
setters used in tests) with functions that mutate state appropriately and then
notify subscribers via the same subscriber notifier so tests re-render
deterministically.

Comment on lines 232 to 236
<BookEventForm
key={key}
timeslot={selectedTimeslot}
shouldRenderCaptcha={shouldRenderCaptcha}
onCancel={() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Prop threading sanity-check

Passing timeslot={selectedTimeslot} to BookEventForm and selectedTimeslot to EventMeta requires updated prop types/usages downstream. If not already done, ensure those components accept and use these props.

Run:

Also applies to: 407-407


🏁 Script executed:

#!/bin/bash
rg -n -A2 -B2 'BookEventForm\\(' packages/features/bookings
rg -n -A2 -B2 'interface .*BookEventForm.*props|type .*BookEventForm.*Props' packages/features/bookings
rg -n -A2 -B2 'EventMeta\\(' packages/features/bookings

Length of output: 904


🏁 Script executed:

#!/bin/bash

# Locate BookEventForm usage sites
rg -n 'BookEventForm\(' -A2 -B2 packages/features/bookings

# Inspect the BookEventFormProps definition and check for timeslot prop
rg -n 'type BookEventFormProps' -A10 packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
rg -n 'timeslot' -n packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx

# Locate EventMeta usage sites
rg -n 'EventMeta\(' -A2 -B2 packages/features/bookings

# Inspect the EventMetaProps (if defined) and check for timeslot handling
rg -n 'type EventMetaProps' -A10 packages/features/bookings
rg -n 'timeslot' -n packages/features/bookings

Length of output: 10075


🏁 Script executed:

#!/bin/bash

# Inspect BookEventForm usage context in Booker.tsx
rg -n 'BookEventForm' -A5 -B5 packages/features/bookings/Booker/Booker.tsx

# Search for EventMeta usages and its prop definitions across the repo
rg -n 'EventMeta' -R .
rg -n 'import.*EventMeta' -R packages/features/bookings
rg -n 'type EventMetaProps' -A10 -R packages/features

Length of output: 2807


🏁 Script executed:

#!/bin/bash

# Find where EventMeta is rendered based on bookerState
rg -n 'bookerState ===' -A5 packages/features/bookings/Booker/Booker.tsx

# Locate EventMeta usage and check passed props
rg -n 'EventMeta' -A5 -B5 packages/features/bookings/Booker/Booker.tsx

# Inspect EventMetaProps definition for timeslot
rg -n 'interface EventMetaProps' -A10 packages/features/bookings/Booker/components/EventMeta.tsx

# Verify if timeslot prop is declared or used in EventMeta component
rg -n 'timeslot' -n packages/features/bookings/Booker/components/EventMeta.tsx

Length of output: 4048


Update EventMeta to accept selectedTimeslot

It looks like you’ve correctly threaded the timeslot prop into BookEventForm (its props type already includes timeslot: string | null), but the EventMeta component itself doesn’t define or consume a selectedTimeslot prop. You’ll need to:

• In packages/features/bookings/Booker/components/EventMeta.tsx, add selectedTimeslot?: string | null (or required) to its props definition.
• Destructure and use selectedTimeslot inside EventMeta wherever you need to display or conditionally render based on the currently selected slot.

This will keep the type system happy and ensure EventMeta actually responds to the timeslot state you’re passing in.

🤖 Prompt for AI Agents
In packages/features/bookings/Booker/components/EventMeta.tsx (where the
component props are defined), EventMeta currently lacks a selectedTimeslot prop
even though the parent passes timeslot/selectedTimeslot; add selectedTimeslot?:
string | null (or selectedTimeslot: string | null if required) to the
component's props type, destructure selectedTimeslot from the props in the
component signature, and then reference it where appropriate (render or
conditionally render UI based on the currently selected slot) so the component
consumes the passed timeslot and the TypeScript types align.

@@ -8,6 +8,7 @@ import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent";
import dayjs from "@calcom/dayjs";
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Critical: onError still writes to the global store; breaks widget isolation

The error path uses useBookerStore.setState to update rescheduleUid and bookingData, which reintroduces the shared global store problem this PR fixes. Route these updates through the context store instead.

Suggested fix (assuming the store exposes actions setRescheduleUid and setBookingData):

@@
-import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
-import { useBookerStore } from "@calcom/features/bookings/Booker/store";
+import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
+import { shallow } from "zustand/shallow";
@@
-  const duration = useBookerStoreContext((state) => state.selectedDuration);
+  const duration = useBookerStoreContext((state) => state.selectedDuration);
+  const [setRescheduleUidCtx, setBookingDataCtx] = useBookerStoreContext(
+    (s) => [s.setRescheduleUid, s.setBookingData],
+    shallow
+  );
@@
-      if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
-        useBookerStore.setState({
-          rescheduleUid: error.data?.rescheduleUid,
-        });
-        useBookerStore.setState({
-          bookingData: {
-            uid: error.data?.rescheduleUid,
-            startTime: error.data?.startTime,
-            attendees: error.data?.attendees,
-          } as unknown as GetBookingType,
-        });
-      }
+      if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
+        setRescheduleUidCtx(error.data.rescheduleUid);
+        setBookingDataCtx({
+          uid: error.data.rescheduleUid,
+          startTime: error.data.startTime,
+          attendees: error.data.attendees,
+        } as unknown as GetBookingType);
+      }

If these actions don't exist yet, please add them to the Booker store (and its type) to keep all mutations instance-scoped.

Also applies to: 305-314


🏁 Script executed:

#!/bin/bash
# Find any remaining direct store mutations that bypass the context (should be none).
rg -n --glob '!**/__tests__/**' 'useBookerStore\.setState' packages | sed 's/^/  /'

Length of output: 297


I’d like to verify whether the context store already exposes the setRescheduleUid and setBookingData actions.


🏁 Script executed:

#!/bin/bash
# Check if context or store files define the requested actions
rg -n 'setRescheduleUid' -A2 packages/features/bookings
rg -n 'setBookingData' -A2 packages/features/bookings

Length of output: 870


Replace direct useBookerStore.setState calls with context actions and add setRescheduleUid

The hook still writes to the global store at lines 305 and 308, re-introducing shared state. We need to route these mutations through the Booker context and add a missing setRescheduleUid action to the store and context.

packages/features/bookings/Booker/components/hooks/useBookings.ts
– Remove useBookerStore.setState calls at lines 305 and 308
– Destructure setRescheduleUid and setBookingData from useBookerStoreContext using zustand/shallow
packages/features/bookings/Booker/store.ts
– Add a setRescheduleUid: (uid: string | null) => void action under the store type (around line 138)
– Implement it to set({ rescheduleUid: uid ?? null }) (mirror setBookingData)
BookerStoreProvider
– Ensure the new action is exposed via the context provider’s value and type

Suggested diff in useBookings.ts:

@@
-import { useBookerStore } from "@calcom/features/bookings/Booker/store";
+import { shallow } from "zustand/shallow";
@@
-  const duration = useBookerStoreContext((state) => state.selectedDuration);
+  const duration = useBookerStoreContext((s) => s.selectedDuration);
+  const [setRescheduleUid, setBookingData] = useBookerStoreContext(
+    (s) => [s.setRescheduleUid, s.setBookingData],
+    shallow
+  );
@@
-      if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
-        useBookerStore.setState({ rescheduleUid: error.data.rescheduleUid });
-        useBookerStore.setState({
-          bookingData: {
-            uid: error.data.rescheduleUid,
-            startTime: error.data.startTime,
-            attendees: error.data.attendees,
-          } as GetBookingType,
-        });
-      }
+      if (
+        error.message === ErrorCode.BookerLimitExceededReschedule &&
+        error.data?.rescheduleUid
+      ) {
+        setRescheduleUid(error.data.rescheduleUid);
+        setBookingData({
+          uid: error.data.rescheduleUid,
+          startTime: error.data.startTime,
+          attendees: error.data.attendees,
+        } as GetBookingType);
+      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/features/bookings/Booker/components/hooks/useBookings.ts around line
11 and usages at lines 305 and 308, remove direct useBookerStore.setState calls
and instead destructure setRescheduleUid and setBookingData from
useBookerStoreContext (import shallow from 'zustand/shallow' and use const {
setRescheduleUid, setBookingData } = useBookerStoreContext(state => ({
setRescheduleUid: state.setRescheduleUid, setBookingData: state.setBookingData
}), shallow)), then replace the two setState calls with calls to
setRescheduleUid(uid) and setBookingData(data); in
packages/features/bookings/Booker/store.ts add a new action setRescheduleUid:
(uid: string | null) => void to the store type around line 138 and implement it
as set({ rescheduleUid: uid ?? null }) mirroring setBookingData; finally ensure
BookerStoreProvider exposes setRescheduleUid in its provided value and type so
the hook can use it.

Comment on lines +196 to +201
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: clearing selected date doesn’t update store state

When selectedDate is falsy, you remove the date query param but return without clearing selectedDate in the store. This causes stale UI state (and affects setMonth which calls this to clear the date).

-    if (!selectedDate) {
-      removeQueryParam("date");
-      return;
-    }
+    if (!selectedDate) {
+      removeQueryParam("date");
+      set({ selectedDate: null });
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
}
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
set({ selectedDate: null });
return;
}
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 196 to 201, when
selectedDate is falsy the code removes the "date" query param but returns
without clearing the store's selectedDate, leaving stale state; update the
branch so before returning it also clears the store's selectedDate (set it to
null/undefined per project convention) while preserving the
omitUpdatingParams/preventMonthSwitching behavior and side effects so the UI and
callers like setMonth no longer see the old date.

Comment on lines +409 to +414
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("duration", selectedDuration ?? "");
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consistency: remove duration query param when unset

Currently setting selectedDuration to null writes an empty duration= param. Prefer removing it entirely for consistency with other unset paths.

-    set({ selectedDuration });
-    if (!get().isPlatform || get().allowUpdatingUrlParams) {
-      updateQueryParam("duration", selectedDuration ?? "");
-    }
+    set({ selectedDuration });
+    if (!get().isPlatform || get().allowUpdatingUrlParams) {
+      if (selectedDuration == null) removeQueryParam("duration");
+      else updateQueryParam("duration", String(selectedDuration));
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("duration", selectedDuration ?? "");
}
},
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
if (selectedDuration == null) {
removeQueryParam("duration");
} else {
updateQueryParam("duration", String(selectedDuration));
}
}
},
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 409 to 414, when
selectedDuration is set to null the code currently writes an empty "duration="
query param; change the call so the query param is removed instead of set empty
— i.e. when selectedDuration is null call the query-removal behavior (pass
undefined/null to updateQueryParam if that removes the key or call the existing
removeQueryParam helper) otherwise pass the numeric value; ensure the
conditional uses selectedDuration === null (or nullish) to trigger removal so
other unset params remain consistent.

Comment on lines +430 to +435
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
}
//removeQueryParam("layout");
},
durationConfig: null,
selectedDuration: null,
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("duration", selectedDuration ?? "");
}
},
setBookingData: (bookingData: GetBookingType | null | undefined) => {
set({ bookingData: bookingData ?? null });
},
recurringEventCount: null,
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
occurenceCount: null,
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
rescheduleUid: null,
bookingData: null,
bookingUid: null,
selectedTimeslot: getQueryParam("slot") || null,
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => {
set({ tentativeSelectedTimeslots });
},
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
},
formValues: {},
setFormValues: (formValues: Record<string, any>) => {
set({ formValues });
},
org: null,
setOrg: (org: string | null | undefined) => {
set({ org });
},
isPlatform: false,
allowUpdatingUrlParams: true,
}));
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consistency: remove slot query param when unsetting timeslot

Writing slot= on null can confuse consumers and complicate parsing. Remove the param instead.

-    set({ selectedTimeslot });
-    if (!get().isPlatform || get().allowUpdatingUrlParams) {
-      updateQueryParam("slot", selectedTimeslot ?? "", false);
-    }
+    set({ selectedTimeslot });
+    if (!get().isPlatform || get().allowUpdatingUrlParams) {
+      if (selectedTimeslot == null) removeQueryParam("slot");
+      else updateQueryParam("slot", selectedTimeslot, false);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
}
//removeQueryParam("layout");
},
durationConfig: null,
selectedDuration: null,
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("duration", selectedDuration ?? "");
}
},
setBookingData: (bookingData: GetBookingType | null | undefined) => {
set({ bookingData: bookingData ?? null });
},
recurringEventCount: null,
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
occurenceCount: null,
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
rescheduleUid: null,
bookingData: null,
bookingUid: null,
selectedTimeslot: getQueryParam("slot") || null,
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => {
set({ tentativeSelectedTimeslots });
},
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
},
formValues: {},
setFormValues: (formValues: Record<string, any>) => {
set({ formValues });
},
org: null,
setOrg: (org: string | null | undefined) => {
set({ org });
},
isPlatform: false,
allowUpdatingUrlParams: true,
}));
},
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
if (selectedTimeslot == null) removeQueryParam("slot");
else updateQueryParam("slot", selectedTimeslot, false);
}
},
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 430 to 435, the code
currently writes "slot=" when selectedTimeslot is null; change the
updateQueryParam call so it removes the param instead of setting it to an empty
string. Concretely, when selectedTimeslot is null/undefined call
updateQueryParam("slot", null, false) (or undefined if that matches the helper's
removal behavior) and otherwise pass the timeslot value; adjust the expression
(e.g. use selectedTimeslot ?? null) so the param is removed rather than set to
an empty string.

Comment on lines +71 to 83
const [bookerState, setBookerState] = useBookerStoreContext(
(state) => [state.state, state.setState],
shallow
);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration);
const setBookingData = useBookerStoreContext((state) => state.setBookingData);
const setOrg = useBookerStoreContext((state) => state.setOrg);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
const setSelectedMonth = useBookerStoreContext((state) => state.setMonth);
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Mixed store usage: onBookerStateChange still subscribes to legacy global store — leaks across widgets

You correctly switched most reads/writes to useBookerStoreContext, but the onBookerStateChange effect still uses useBookerStore.subscribe/getState (legacy global store). That will cause callbacks to fire for changes from other widget instances.

Replace the legacy subscription with a context-scoped selection so the callback tracks only this instance.

Apply these diffs:

  1. Type the selector against the store type (and decouple from legacy hook type):
@@
-  const getStateValues = useCallback(
-    (state: ReturnType<typeof useBookerStore.getState>): BookerStoreValues => {
+  const getStateValues = useCallback(
+    (state: BookerStore): BookerStoreValues => {
     return Object.fromEntries(
       Object.entries(state).filter(([_, value]) => typeof value !== "function")
     ) as BookerStoreValues;
   }, []);
  1. Replace the legacy subscription with a context-scoped effect:
@@
-  useEffect(() => {
-    if (!onBookerStateChange) return;
-
-    const unsubscribe = useBookerStore.subscribe((state) => {
-      const currentStateValues = getStateValues(state);
-      debouncedStateChange(currentStateValues, onBookerStateChange);
-    });
-
-    // Initial call with current state
-    const initialState = getStateValues(useBookerStore.getState());
-    onBookerStateChange(initialState);
-    prevStateRef.current = initialState;
-
-    return () => {
-      unsubscribe();
-      debouncedStateChange.cancel();
-    };
-  }, [onBookerStateChange, getStateValues, debouncedStateChange]);
+  // Drive the callback from this component's scoped store values
+  const scopedStateValues = useBookerStoreContext(getStateValues);
+  useEffect(() => {
+    if (!onBookerStateChange) return;
+    debouncedStateChange(scopedStateValues, onBookerStateChange);
+    return () => {
+      debouncedStateChange.cancel();
+    };
+  }, [scopedStateValues, onBookerStateChange, debouncedStateChange]);

Add the necessary type import outside the changed hunk:

import type { BookerStore } from "@calcom/features/bookings/Booker/store";

Also remove the legacy useBookerStore import (see next comment).

🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 71 to
83, the effect still subscribes to the legacy global useBookerStore causing
cross-widget leaks; replace that subscription with a context-scoped selector
typed to the store and use a useEffect that subscribes via useBookerStoreContext
so callbacks only see this instance's state, type the selector against
BookerStore (import type { BookerStore } from
"@calcom/features/bookings/Booker/store"), and remove the legacy useBookerStore
import; ensure you update the selector signature to use the BookerStore type and
wire the new context-scoped subscription inside a useEffect with proper cleanup.

Comment on lines +181 to +197
useInitializeBookerStoreContext({
...props,
teamMemberEmail,
crmAppSlug,
crmOwnerRecordType,
crmRecordId: props.crmRecordId,
eventId: event?.data?.id,
rescheduleUid: props.rescheduleUid ?? null,
bookingUid: props.bookingUid ?? null,
layout: layout,
org: props.entity?.orgSlug,
username,
bookingData,
isPlatform: true,
allowUpdatingUrlParams,
});
const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Double initialization: remove legacy useInitializeBookerStore call to avoid cross-instance coupling

You correctly initialize the context store with useInitializeBookerStoreContext here. However, the file still calls the legacy useInitializeBookerStore above (Lines 164–180), which initializes the global store and reintroduces cross-widget coupling.

  • Remove the legacy initializer call block (Lines 164–180).
  • Drop the legacy imports.

Suggested import cleanup outside this hunk:

-import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
+import type { BookerStore } from "@calcom/features/bookings/Booker/store";

And delete the legacy initializer block:

-  useInitializeBookerStore({
-    ...props,
-    teamMemberEmail,
-    crmAppSlug,
-    crmOwnerRecordType,
-    crmRecordId: props.crmRecordId,
-    eventId: event?.data?.id,
-    rescheduleUid: props.rescheduleUid ?? null,
-    bookingUid: props.bookingUid ?? null,
-    layout: layout,
-    org: props.entity?.orgSlug,
-    username,
-    bookingData,
-    isPlatform: true,
-    allowUpdatingUrlParams,
-  });
🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 164 to
180 (legacy initializer) and 181 to 197 (current initializer), the legacy
useInitializeBookerStore call remains and causes global/store cross-instance
coupling; remove the entire legacy initializer block (lines 164–180) and any
associated legacy imports at the top of the file, leaving only the
useInitializeBookerStoreContext invocation shown at ~181–197, and ensure imports
no longer reference the removed legacy initializer to avoid unused imports or
lint errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛 bug Something isn't working consumer core area: core, team members only platform Anything related to our platform plan
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants