diff --git a/apps/web/src/components/auth/segment-gate.tsx b/apps/web/src/components/auth/segment-gate.tsx new file mode 100644 index 0000000..431596f --- /dev/null +++ b/apps/web/src/components/auth/segment-gate.tsx @@ -0,0 +1,145 @@ +import { SegmentQualifier } from "@/components/auth/segment-qualifier"; +import { useAuth } from "@/hooks/use-auth"; +import { capture, setPeopleProperties } from "@/lib/posthog"; +import { Button } from "@sase/ui"; +import { useLocation } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; + +/** + * Universal post-auth segment gate. + * + * The register page gates segment selection behind the form, but that only + * covers email + register-page-OAuth signups. A user who starts Google OAuth + * from the LOGIN page (login.tsx) gets an account created server-side and lands + * on the dashboard with no segment ever chosen — this is the bulk of the ~41% + * of signups with a null segment. Segment isn't persisted server-side (only + * localStorage + a PostHog person property), so we enforce it here at the one + * chokepoint every authenticated user passes through: the dashboard. + * + * Behaviour (product decision 2026-07-27): + * - New users (account created in the last few minutes → the login-OAuth gap): + * MANDATORY, non-dismissible modal. + * - Existing segment-less users (the ~538 backfill): SOFT, dismissible with a + * cooldown so we re-ask later without nagging or blocking active/paying users. + * + * This is a baseline fix shipped to everyone — NOT the `funnel-bucket` A/B + * experiment (that gates the tailored value/upgrade experience separately). + */ +const SEGMENT_KEY = "sase-b2b-segment"; +const DISMISS_UNTIL_KEY = "sase-b2b-segment-prompt-until"; +// Account younger than this ⇒ treat as a fresh signup (mandatory). Generous so +// the signup → first dashboard load always counts, even with a slow OAuth hop. +const NEW_USER_WINDOW_MS = 15 * 60 * 1000; +// How long a "Daha sonra" dismissal silences the soft prompt for existing users. +const DISMISS_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; + +function readSegment(): string | null { + try { + return localStorage.getItem(SEGMENT_KEY); + } catch { + return null; + } +} + +export function SegmentGate() { + const { user, isLoading } = useAuth(); + const location = useLocation(); + const [open, setOpen] = useState(false); + const [mandatory, setMandatory] = useState(false); + + useEffect(() => { + // Only inside the app, only once the session has resolved to a real user. + if (isLoading || !user) { + setOpen(false); + return; + } + if (!location.pathname.startsWith("/dashboard")) { + setOpen(false); + return; + } + // Already segmented — nothing to do. (Register writes this pre-redirect, so + // email + register-OAuth signups never see the gate.) + if (readSegment()) { + setOpen(false); + return; + } + + const createdAt = user.createdAt ? new Date(user.createdAt).getTime() : 0; + const isNew = createdAt > 0 && Date.now() - createdAt < NEW_USER_WINDOW_MS; + + if (isNew) { + setMandatory(true); + setOpen(true); + return; + } + + // Existing segment-less user: honour the soft-dismiss cooldown. + let dismissedUntil = 0; + try { + dismissedUntil = Number(localStorage.getItem(DISMISS_UNTIL_KEY)) || 0; + } catch { + dismissedUntil = 0; + } + if (Date.now() < dismissedUntil) { + setOpen(false); + return; + } + setMandatory(false); + setOpen(true); + }, [user, isLoading, location.pathname]); + + if (!open) return null; + + function handleSelect(seg: string) { + try { + localStorage.setItem(SEGMENT_KEY, seg); + } catch { + // localStorage unavailable — still fire analytics below. + } + // `source` distinguishes the login-OAuth gap fill from the backfill so we can + // measure each in PostHog; mirrors register.tsx's signup_segment_selected. + capture("signup_segment_selected", { + segment: seg, + source: mandatory ? "post_signup_gate" : "backfill_gate", + }); + setPeopleProperties({ b2b_segment: seg }); + setOpen(false); + } + + function handleDismiss() { + try { + localStorage.setItem(DISMISS_UNTIL_KEY, String(Date.now() + DISMISS_COOLDOWN_MS)); + } catch { + // best-effort + } + capture("signup_segment_prompt_dismissed"); + setOpen(false); + } + + return ( +
+ İşletmeni seç — sana uygun sınırsız şase sorgulama erişimini açalım. +
+- {t("subscription.valueUpsell.title")} + {tailored ? tailored.title : t("subscription.valueUpsell.title")}
- {t("subscription.valueUpsell.description")} + {tailored ? tailored.desc : t("subscription.valueUpsell.description")}