From 208b096cb17e1838ea5c0c65f3dd383329fefb66 Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Fri, 22 May 2026 13:21:22 +0300 Subject: [PATCH] chore(flows): defensive gate on interactive bubble render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/components/inbox/message-bubble.tsx | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/inbox/message-bubble.tsx b/src/components/inbox/message-bubble.tsx index a98beb2..54dbac3 100644 --- a/src/components/inbox/message-bubble.tsx +++ b/src/components/inbox/message-bubble.tsx @@ -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 }) { ); - 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 ( +

+ {message.content_text || "[Interactive reply]"} +

+ ); + } // 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 }) {

); + } default: return (