Here’s a clean, production-ready SurveyModal you can drop into your app. It’s shadcn-native, accessible, stores results locally (so you don’t keep nagging), and exposes a hook so you can also save to Firestore/backend if you want.

I’m giving you full files (no partials), plus a tiny integration patch for your BusinessPlanPage.

1) components/SurveyModal.tsx
import * as React from "react";
import { createPortal } from "react-dom";
import { X } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogClose,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";

export type SurveyOption = {
  id: string;
  label: string;
};

export type SurveyPayload = {
  surveyId: string;
  selectedOptionId: string;
  selectedLabel: string;
  createdAt: string; // ISO
  userId?: string | null;
  planId?: string | null;
};

type SurveyModalProps = {
  open: boolean;
  onOpenChange: (v: boolean) => void;

  /** A unique ID for this survey (used for localStorage “already answered”). */
  surveyId: string;

  /** Main title & description */
  title: string;
  description?: string;

  /** Radio options */
  options: SurveyOption[];

  /** Called when user submits (send to Firestore/DB/analytics here). */
  onSubmit?: (payload: SurveyPayload) => Promise<void> | void;

  /** Optional: pass current user/plan for analytics */
  userId?: string | null;
  planId?: string | null;

  /** Optional CSS class for the wrapper */
  className?: string;
};

export function SurveyModal(props: SurveyModalProps) {
  const {
    open,
    onOpenChange,
    surveyId,
    title,
    description,
    options,
    onSubmit,
    userId = null,
    planId = null,
    className,
  } = props;

  const [selected, setSelected] = React.useState<string>("");
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState<string | null>(null);

  React.useEffect(() => {
    if (!open) {
      setSelected("");
      setError(null);
    }
  }, [open]);

  const handleSubmit = async () => {
    setError(null);
    if (!selected) {
      setError("Please select an option to continue.");
      return;
    }
    try {
      setSubmitting(true);

      const chosen = options.find((o) => o.id === selected)!;
      const payload: SurveyPayload = {
        surveyId,
        selectedOptionId: chosen.id,
        selectedLabel: chosen.label,
        createdAt: new Date().toISOString(),
        userId,
        planId,
      };

      // Persist: local “answered” flag
      try {
        const key = `survey:${surveyId}:answered`;
        localStorage.setItem(key, "true");
      } catch {}

      // Callback to client code (DB, analytics, etc.)
      if (onSubmit) {
        await onSubmit(payload);
      }

      onOpenChange(false);
    } catch (e: any) {
      setError(e?.message || "Something went wrong. Please try again.");
    } finally {
      setSubmitting(false);
    }
  };

  return createPortal(
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className={cn("max-w-lg", className)} aria-describedby={undefined}>
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          {description ? (
            <DialogDescription>{description}</DialogDescription>
          ) : null}
        </DialogHeader>

        <div className="space-y-4">
          <Separator />
          <fieldset className="space-y-3">
            {options.map((opt) => (
              <label
                key={opt.id}
                className={cn(
                  "flex items-center gap-3 rounded-md border p-3 cursor-pointer hover:bg-muted",
                  selected === opt.id ? "border-primary" : "border-border"
                )}
              >
                <input
                  type="radio"
                  name={`survey-${surveyId}`}
                  className="h-4 w-4"
                  value={opt.id}
                  checked={selected === opt.id}
                  onChange={() => setSelected(opt.id)}
                  aria-labelledby={`survey-label-${opt.id}`}
                />
                <Label id={`survey-label-${opt.id}`} className="font-normal cursor-pointer">
                  {opt.label}
                </Label>
              </label>
            ))}
          </fieldset>

          {error ? (
            <p className="text-sm text-destructive" role="alert">
              {error}
            </p>
          ) : null}
        </div>

        <DialogFooter className="mt-2">
          <DialogClose asChild>
            <Button variant="ghost">
              <X className="h-4 w-4 mr-2" />
              Close
            </Button>
          </DialogClose>
          <Button onClick={handleSubmit} disabled={submitting}>
            {submitting ? "Submitting..." : "Submit"}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>,
    document.body
  );
}

/** Utility to check if a survey has already been answered on this device. */
export function hasAnsweredSurvey(surveyId: string): boolean {
  try {
    return localStorage.getItem(`survey:${surveyId}:answered`) === "true";
  } catch {
    return false;
  }
}

2) stores/useSurveyStore.ts
import { create } from "zustand";

type SurveyState = {
  open: boolean;
  surveyId: string | null;
  title: string;
  description?: string;
  options: { id: string; label: string }[];
  context: { userId?: string | null; planId?: string | null };
  set: (s: Partial<SurveyState>) => void;
  reset: () => void;
};

export const useSurveyStore = create<SurveyState>((set) => ({
  open: false,
  surveyId: null,
  title: "",
  description: undefined,
  options: [],
  context: {},
  set: (s) => set(s),
  reset: () =>
    set({
      open: false,
      surveyId: null,
      title: "",
      description: undefined,
      options: [],
      context: {},
    }),
}));

3) Backend stub (optional): lib/surveys.ts

Use this to centralize saving. You can swap the body to Firestore/your DB later.

import type { SurveyPayload } from "@/components/SurveyModal";

export async function submitSurvey(payload: SurveyPayload) {
  // Replace this with your real API/Firestore call.
  try {
    await fetch("/api/surveys/submit", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
  } catch (e) {
    // non-fatal
    console.warn("Survey submit failed (will still close modal):", e);
  }
}

4) Integrate in BusinessPlanPage.tsx

Here’s the minimal patch to show the survey after the first successful generation (Lean or Full). It only appears once per device for this survey.

// --- imports (add these) ---
import { SurveyModal, hasAnsweredSurvey, type SurveyOption } from "@/components/SurveyModal";
import { useSurveyStore } from "@/stores/useSurveyStore";
import { submitSurvey } from "@/lib/surveys";

// (inside component)
const survey = useSurveyStore();
const SURVEY_ID = "discovery-channel-v1";

const SURVEY_OPTIONS: SurveyOption[] = [
  { id: "search", label: "Google/Bing Search" },
  { id: "ai", label: "AI Platform (ChatGPT, Claude, Gemini, etc.)" },
  { id: "facebook", label: "Facebook" },
  { id: "reddit", label: "Reddit" },
  { id: "instagram", label: "Instagram" },
  { id: "youtube", label: "YouTube" },
  { id: "blog", label: "Blog / Article / Publication" },
  { id: "review", label: "Review or comparison site" },
  { id: "university", label: "Through my University" },
  { id: "peer", label: "Recommendation from peer/advisor" },
  { id: "store", label: "QuickBooks/Xero App Store" },
  { id: "other", label: "Other" },
];

// After a successful generation:
const onSubmit = async (values: BusinessPlanFormValues) => {
  setIsGenerating(true);
  setError(null);
  setPlan(null);

  try {
    const request: PlanRequest = { /* ...existing fields... */ mode: activeTab, /* etc. */ };
    const response = await generatePlan(request);
    setPlan(response);

    // 🔔 Trigger survey once per device
    if (!hasAnsweredSurvey(SURVEY_ID)) {
      survey.set({
        open: true,
        surveyId: SURVEY_ID,
        title: "How did you first hear about IBrandBiz?",
        description: "Pick the option that best describes how you found us.",
        options: SURVEY_OPTIONS,
        context: { userId: currentUser?.uid ?? null, planId: undefined },
      });
    }
  } catch (err) {
    console.error("Failed to generate business plan:", err);
    setError("Couldn't generate your business plan right now. Please try again.");
  } finally {
    setIsGenerating(false);
  }
};

// ...near your return() JSX, place the modal once:
<SurveyModal
  open={survey.open}
  onOpenChange={(v) => survey.set({ open: v })}
  surveyId={survey.surveyId ?? "unknown"}
  title={survey.title}
  description={survey.description}
  options={survey.options}
  userId={survey.context.userId}
  planId={survey.context.planId}
  onSubmit={submitSurvey}
/>