fix: hide feature opt-in feedback dialog during impersonation (#27802)
* fix: hide feature opt-in banner and feedback forms during impersonation Co-Authored-By: sean@cal.com <Sean@brydon.io> * fix: keep opt-in banner visible during impersonation, only hide feedback forms Co-Authored-By: sean@cal.com <Sean@brydon.io> * fix: allow feedback form during impersonation in development mode Co-Authored-By: sean@cal.com <Sean@brydon.io> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
sean@cal.com
Devin AI
parent
c58e1f4ae5
commit
c3a8301b0c
@@ -0,0 +1,92 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useFeatureOptInBanner } from "./useFeatureOptInBanner";
|
||||
|
||||
const mockUseSession = vi.fn();
|
||||
vi.mock("next-auth/react", () => ({
|
||||
useSession: () => mockUseSession(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/features/feature-opt-in/config", () => ({
|
||||
getOptInFeatureConfig: vi.fn(() => ({
|
||||
slug: "bookings-v3",
|
||||
i18n: { title: "t", name: "n", description: "d" },
|
||||
bannerImage: { src: "/img.png", width: 100, height: 100 },
|
||||
policy: "permissive",
|
||||
displayLocations: ["banner", "settings"],
|
||||
})),
|
||||
shouldDisplayFeatureAt: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/feature-opt-in-storage", () => ({
|
||||
getFeatureOptInTimestamp: vi.fn(() => null),
|
||||
isFeatureDismissed: vi.fn(() => false),
|
||||
setFeatureDismissed: vi.fn(),
|
||||
setFeatureOptedIn: vi.fn(),
|
||||
isFeatureFeedbackShown: vi.fn(() => false),
|
||||
setFeatureFeedbackShown: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockMutateAsync = vi.fn();
|
||||
const mockInvalidate = vi.fn();
|
||||
vi.mock("@calcom/trpc/react", () => ({
|
||||
trpc: {
|
||||
useUtils: vi.fn(() => ({
|
||||
viewer: {
|
||||
featureOptIn: {
|
||||
checkFeatureOptInEligibility: { invalidate: mockInvalidate },
|
||||
listForUser: { invalidate: mockInvalidate },
|
||||
},
|
||||
},
|
||||
})),
|
||||
viewer: {
|
||||
featureOptIn: {
|
||||
checkFeatureOptInEligibility: {
|
||||
useQuery: vi.fn(() => ({
|
||||
data: {
|
||||
status: "can_opt_in",
|
||||
canOptIn: true,
|
||||
blockingReason: null,
|
||||
userRoleContext: { isOrgAdmin: false, orgId: null, adminTeamIds: [], adminTeamNames: [] },
|
||||
},
|
||||
isLoading: false,
|
||||
})),
|
||||
},
|
||||
setUserState: { useMutation: vi.fn(() => ({ mutateAsync: mockMutateAsync })) },
|
||||
setTeamState: { useMutation: vi.fn(() => ({ mutateAsync: mockMutateAsync })) },
|
||||
setOrganizationState: { useMutation: vi.fn(() => ({ mutateAsync: mockMutateAsync })) },
|
||||
setUserAutoOptIn: { useMutation: vi.fn(() => ({ mutateAsync: mockMutateAsync })) },
|
||||
setTeamAutoOptIn: { useMutation: vi.fn(() => ({ mutateAsync: mockMutateAsync })) },
|
||||
setOrganizationAutoOptIn: { useMutation: vi.fn(() => ({ mutateAsync: mockMutateAsync })) },
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: { capture: vi.fn() },
|
||||
}));
|
||||
|
||||
describe("useFeatureOptInBanner", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows banner for normal users when eligible", () => {
|
||||
mockUseSession.mockReturnValue({ data: { user: {} } });
|
||||
|
||||
const { result } = renderHook(() => useFeatureOptInBanner("bookings-v3"));
|
||||
|
||||
expect(result.current.shouldShow).toBe(true);
|
||||
});
|
||||
|
||||
it("still shows banner when user is impersonated", () => {
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { user: { impersonatedBy: { id: 999, email: "admin@cal.com" } } },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFeatureOptInBanner("bookings-v3"));
|
||||
|
||||
expect(result.current.shouldShow).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { OptInFeatureConfig } from "@calcom/features/feature-opt-in/config";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useOptInFeedback } from "./useOptInFeedback";
|
||||
|
||||
const mockUseSession = vi.fn();
|
||||
vi.mock("next-auth/react", () => ({
|
||||
useSession: () => mockUseSession(),
|
||||
}));
|
||||
|
||||
const mockEnv = vi.hoisted(() => ({ isENVDev: false }));
|
||||
vi.mock("@calcom/lib/env", () => ({
|
||||
get isENVDev() {
|
||||
return mockEnv.isENVDev;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../lib/feature-opt-in-storage", () => ({
|
||||
getFeatureOptInTimestamp: vi.fn(() => Date.now() - 10 * 24 * 60 * 60 * 1000),
|
||||
isFeatureFeedbackShown: vi.fn(() => false),
|
||||
setFeatureFeedbackShown: vi.fn(),
|
||||
}));
|
||||
|
||||
const featureConfig: OptInFeatureConfig = {
|
||||
slug: "bookings-v3" as OptInFeatureConfig["slug"],
|
||||
i18n: { title: "t", name: "n", description: "d" },
|
||||
bannerImage: { src: "/img.png", width: 100, height: 100 },
|
||||
policy: "permissive",
|
||||
formbricks: {
|
||||
waitAfterDays: 0,
|
||||
surveyId: "survey-1",
|
||||
questions: { ratingQuestionId: "q1", commentQuestionId: "q2" },
|
||||
},
|
||||
};
|
||||
|
||||
describe("useOptInFeedback", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
mockUseSession.mockReturnValue({ data: { user: {} } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows feedback dialog for normal users after delay", () => {
|
||||
const { result } = renderHook(() => useOptInFeedback("bookings-v3", featureConfig));
|
||||
expect(result.current.showFeedbackDialog).toBe(false);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(6000);
|
||||
});
|
||||
|
||||
expect(result.current.showFeedbackDialog).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show feedback dialog when user is impersonated in production", () => {
|
||||
mockEnv.isENVDev = false;
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { user: { impersonatedBy: { id: 999, email: "admin@cal.com" } } },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useOptInFeedback("bookings-v3", featureConfig));
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(6000);
|
||||
});
|
||||
|
||||
expect(result.current.showFeedbackDialog).toBe(false);
|
||||
});
|
||||
|
||||
it("shows feedback dialog when user is impersonated in development", () => {
|
||||
mockEnv.isENVDev = true;
|
||||
mockUseSession.mockReturnValue({
|
||||
data: { user: { impersonatedBy: { id: 999, email: "admin@cal.com" } } },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useOptInFeedback("bookings-v3", featureConfig));
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(6000);
|
||||
});
|
||||
|
||||
expect(result.current.showFeedbackDialog).toBe(true);
|
||||
});
|
||||
|
||||
it("shows feedback dialog when session has no impersonation", () => {
|
||||
mockUseSession.mockReturnValue({ data: { user: { name: "Test" } } });
|
||||
|
||||
const { result } = renderHook(() => useOptInFeedback("bookings-v3", featureConfig));
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(6000);
|
||||
});
|
||||
|
||||
expect(result.current.showFeedbackDialog).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { OptInFeatureConfig } from "@calcom/features/feature-opt-in/config";
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
getFeatureOptInTimestamp,
|
||||
@@ -31,10 +33,9 @@ export interface OptInFeedbackState {
|
||||
* Hook to manage feedback dialog display after a user opts into a feature.
|
||||
* Shows a custom feedback dialog after a configurable delay (waitAfterDays).
|
||||
*/
|
||||
function useOptInFeedback(
|
||||
featureId: string,
|
||||
featureConfig: OptInFeatureConfig | null
|
||||
): OptInFeedbackState {
|
||||
function useOptInFeedback(featureId: string, featureConfig: OptInFeatureConfig | null): OptInFeedbackState {
|
||||
const { data: session } = useSession();
|
||||
const isImpersonating = !!session?.user?.impersonatedBy;
|
||||
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
|
||||
const hasTriggeredRef = useRef(false);
|
||||
|
||||
@@ -44,6 +45,7 @@ function useOptInFeedback(
|
||||
}, [featureId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isImpersonating && !isENVDev) return;
|
||||
if (!featureConfig?.formbricks) return;
|
||||
|
||||
// Don't trigger if already triggered this session
|
||||
@@ -77,7 +79,7 @@ function useOptInFeedback(
|
||||
// Show after page load delay to let the page finish loading
|
||||
const timer = setTimeout(triggerFeedback, PAGE_LOAD_DELAY_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [featureId, featureConfig]);
|
||||
}, [featureId, featureConfig, isImpersonating]);
|
||||
|
||||
const feedbackDialogProps = featureConfig?.formbricks?.surveyId
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user