* feat: add delayed formbricks tracking for feature opt-in Adds delayed Formbricks survey tracking for feature opt-in. When a user opts into a feature, this allows triggering a Formbricks action after a configurable delay (e.g., 24 hours later) to collect feedback once they've had time to use the feature. Key changes: - Added `formbricks` config option to `OptInFeatureConfig` interface with `actionName` and `delayMs` properties - Created `useFormbricksOptInTracking` hook that handles the delayed tracking logic - Added `isFeatureTracked` / `setFeatureTracked` storage helpers to prevent duplicate tracking - Integrated the tracking hook into `useFeatureOptInBanner` Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * upgrade formbricks * feat: replace formbricks popup with custom feedback dialog Instead of using Formbricks' built-in popup, we now show a custom Cal.com-styled feedback dialog that submits responses directly to Formbricks API via tRPC mutation. - Add FeedbackDialog component with emoji rating selector - Add feedback tRPC router for server-side Formbricks submission - Update useFormbricksOptInTracking to return dialog state - Add survey config fields (surveyId, questions) to config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: position feedback dialog at bottom-right corner - Use base-ui Dialog primitives for custom positioning - Position dialog at bottom-right to avoid Intercom overlap - Use z-index 10000 (below Intercom's high z-index) - Keep blocking backdrop for modal behavior - Use i18n keys for title/description - Add survey IDs for bookings-v3 feedback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add i18n keys for feedback dialog title/description Allow each feature to specify custom i18n keys for the feedback dialog title and description via the formbricks config. - Add titleKey/descriptionKey to formbricks config interface - Pass i18n keys through feedbackDialogProps - Add bookings_v3_feedback_title/description translation keys Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: move FeedbackDialog into FeatureOptInBannerWrapper Better encapsulation - consumers of the feature opt-in banner no longer need to handle the feedback dialog separately. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add 5 second delay before showing feedback dialog Ensures the page has time to finish loading before showing the feedback dialog, avoiding showing it while skeletons are still visible. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: simplify feedback dialog UI - Remove redundant question labels - Add "(optional)" to comment placeholder Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove emoji button borders and add footer gap - Remove borders from rating emoji buttons - Add proper gap between textarea and footer (pb-4) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: delayMs is opt-in waiting period, not setTimeout delay delayMs represents the minimum time that must pass since opt-in before showing the feedback form (e.g., 3 days). If not enough time has passed, we skip showing the form entirely instead of setting a long setTimeout. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: custom feedback dialog for feature opt-in - Replace Formbricks popup with Cal.com-styled dialog - Add configurable delay (waitAfterDays) before showing feedback - Position dialog at bottom-right, non-blocking - Add localStorage tracking to prevent duplicate feedback - Add device targeting (showOn: desktop/mobile/all) - Create tRPC endpoint for Formbricks API submission - Use proper logger for error handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: rename tracking terminology to feedback - Rename useFormbricksOptInTracking → useOptInFeedback - Rename FormbricksOptInTrackingResult → OptInFeedbackState - Rename formbricksTracking property → feedback - Rename FormbricksTrackingState → FeedbackState We no longer "track" events to Formbricks. Instead, we show our custom feedback dialog when conditions are met. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: set waitAfterDays to 3 for production feedback delay Co-Authored-By: unknown <> * fix: update formbricks JS SDK usage for v3.0.0 The @formbricks/js SDK v3.0.0 changed its API: - setup() no longer accepts debug, userId, or attributes - Use setUserId() and setAttributes() after setup instead - track() now expects { hiddenFields: ... } or undefined Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import type { OptInFeatureConfig } from "@calcom/features/feature-opt-in/config";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import {
|
|
getFeatureOptInTimestamp,
|
|
isFeatureFeedbackShown,
|
|
setFeatureFeedbackShown,
|
|
} from "../lib/feature-opt-in-storage";
|
|
|
|
/** Delay before showing the feedback dialog to let the page finish loading */
|
|
const PAGE_LOAD_DELAY_MS = 5000;
|
|
|
|
export interface OptInFeedbackState {
|
|
/** Whether the feedback dialog should be shown */
|
|
showFeedbackDialog: boolean;
|
|
/** Props to pass to the FeedbackDialog component */
|
|
feedbackDialogProps: {
|
|
surveyId: string;
|
|
ratingQuestionId: string;
|
|
commentQuestionId: string;
|
|
titleKey?: string;
|
|
descriptionKey?: string;
|
|
showOn?: "all" | "desktop" | "mobile";
|
|
} | null;
|
|
/** Call this when the dialog is closed (either submitted or skipped) */
|
|
onFeedbackComplete: () => void;
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
|
|
const hasTriggeredRef = useRef(false);
|
|
|
|
const onFeedbackComplete = useCallback(() => {
|
|
setShowFeedbackDialog(false);
|
|
setFeatureFeedbackShown(featureId);
|
|
}, [featureId]);
|
|
|
|
useEffect(() => {
|
|
if (!featureConfig?.formbricks) return;
|
|
|
|
// Don't trigger if already triggered this session
|
|
if (hasTriggeredRef.current) return;
|
|
|
|
// Don't show if feedback was already shown (persisted in localStorage)
|
|
const alreadyShown = isFeatureFeedbackShown(featureId);
|
|
if (alreadyShown) return;
|
|
|
|
// Don't trigger if user hasn't opted in yet
|
|
const optInTimestamp = getFeatureOptInTimestamp(featureId);
|
|
if (!optInTimestamp) return;
|
|
|
|
// Don't trigger if survey is not configured
|
|
const { surveyId, questions } = featureConfig.formbricks;
|
|
if (!surveyId || !questions?.ratingQuestionId || !questions?.commentQuestionId) return;
|
|
|
|
const { waitAfterDays } = featureConfig.formbricks;
|
|
const waitAfterMs = waitAfterDays * 24 * 60 * 60 * 1000;
|
|
const timeSinceOptIn = Date.now() - optInTimestamp;
|
|
|
|
// Don't show feedback if not enough time has passed since opt-in
|
|
// (e.g., wait 3 days after opt-in before asking for feedback)
|
|
if (timeSinceOptIn < waitAfterMs) return;
|
|
|
|
const triggerFeedback = (): void => {
|
|
hasTriggeredRef.current = true;
|
|
setShowFeedbackDialog(true);
|
|
};
|
|
|
|
// Show after page load delay to let the page finish loading
|
|
const timer = setTimeout(triggerFeedback, PAGE_LOAD_DELAY_MS);
|
|
return () => clearTimeout(timer);
|
|
}, [featureId, featureConfig]);
|
|
|
|
const feedbackDialogProps = featureConfig?.formbricks?.surveyId
|
|
? {
|
|
surveyId: featureConfig.formbricks.surveyId,
|
|
ratingQuestionId: featureConfig.formbricks.questions.ratingQuestionId,
|
|
commentQuestionId: featureConfig.formbricks.questions.commentQuestionId,
|
|
titleKey: featureConfig.formbricks.titleKey,
|
|
descriptionKey: featureConfig.formbricks.descriptionKey,
|
|
showOn: featureConfig.formbricks.showOn,
|
|
}
|
|
: null;
|
|
|
|
return {
|
|
showFeedbackDialog,
|
|
feedbackDialogProps,
|
|
onFeedbackComplete,
|
|
};
|
|
}
|
|
|
|
export { useOptInFeedback };
|