chore(flows): defensive gate on interactive bubble render

Wraps the new `case 'interactive'` rendering with `isFlowsEnabled(profile)`
from the beta-features module that landed in #113. Non-beta accounts
fall back to the plain-text rendering for `content_type='interactive'`
rows — they get the option title as text, no special "↩ Button reply"
affordance.

Why this gate exists even though the case is unreachable today:
  - No code path on this branch produces `content_type='interactive'`
    rows. The flow runner that does is upcoming (PR #2), and it'll be
    behind the same gate.
  - But: if a stray interactive row ever shows up in a non-beta
    account (manual SQL insert, data import, future bug), we'd rather
    the account see boring plain text than UI for a feature they
    haven't opted into.
  - The cost is ~3 LoC and one hook call. Pure paranoia insurance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arnas Donauskas 2026-05-22 13:21:22 +03:00
parent dc245d5914
commit 208b096cb1
No known key found for this signature in database

View File

@ -17,6 +17,8 @@ import {
import { format } from "date-fns";
import { ReplyQuote } from "./reply-quote";
import { MessageReactions } from "./message-reactions";
import { useAuth } from "@/hooks/use-auth";
import { isFlowsEnabled } from "@/lib/flows/feature-flag";
interface MessageBubbleProps {
message: Message;
@ -117,6 +119,14 @@ function MediaImage({ url, alt }: { url: string; alt: string }) {
}
function MessageContent({ message }: { message: Message }) {
// Used to gate the interactive-reply bubble style below. The webhook
// only ever inserts `content_type='interactive'` rows when our runner
// sent an interactive message first — and the runner itself is behind
// the same beta gate (forthcoming PRs) — so this gate is belt-and-
// suspenders against any stray row that slipped in via manual SQL,
// a misconfigured import, or a future bug. Non-beta accounts fall
// back to plain text.
const { profile } = useAuth();
switch (message.content_type) {
case "text":
return (
@ -213,7 +223,19 @@ function MessageContent({ message }: { message: Message }) {
</div>
);
case "interactive":
case "interactive": {
// Defensive gate — non-beta accounts render the tapped option as
// plain text. See the comment at the top of MessageContent for why
// this can't be reached in normal operation; this branch exists so
// that if a stray interactive row ever appears for a non-beta
// account, they don't see UI for a feature they haven't opted into.
if (!isFlowsEnabled(profile)) {
return (
<p className="whitespace-pre-wrap break-words text-sm">
{message.content_text || "[Interactive reply]"}
</p>
);
}
// Customer tapped a reply button or list row on a message the bot
// sent. We show the tapped option's title (already in content_text,
// set by parseMessageContent in the webhook) with a small affordance
@ -230,6 +252,7 @@ function MessageContent({ message }: { message: Message }) {
</p>
</div>
);
}
default:
return (