Add a floating help FAB (bottom-right via Stack) that opens a Bottom Sheet with three options: FAQ search powered by a Firestore faq collection with TextField filtering and accordion-style expand, a ticket submission form that writes to Firestore support_tickets and triggers a Cloud Function email via SendGrid, and live chat via an Intercom WebView Custom Widget that lazy-loads only when the user taps Live Chat to avoid wasting memory.
Floating support widget with FAQ search, ticket form, and live chat
Most apps need a support system, but building one from scratch is complex. This tutorial creates a floating help button that stays in the bottom-right corner of every page. Tapping it opens a Bottom Sheet with three options: search your FAQ database, submit a support ticket, or start a live chat. The FAQ search queries a Firestore faq collection using keyword matching and displays results in expandable accordion items with category filtering. The ticket form validates input and creates a Firestore document that triggers a Cloud Function to email your support team via SendGrid. Live chat embeds Intercom's messenger in a WebView Custom Widget — critically, the WebView only initializes when the user actually taps Live Chat, avoiding the 50-100MB memory cost of an eagerly loaded WebView.
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- Firebase Authentication enabled with users signing in
- An Intercom account with a Messenger URL for the live chat feature
- A SendGrid account and API key for the Cloud Function email trigger
- FlutterFlow Pro plan (Custom Widget required for the Intercom WebView)
Step-by-step guide
Add a FloatingActionButton positioned bottom-right using a page-level Stack
Add a FloatingActionButton positioned bottom-right using a page-level Stack
Wrap your main page content in a Stack widget. The first child is your existing page content (Column or ScrollView). The second child is an Align widget set to bottomRight with padding 16 from bottom and right. Inside the Align, place a Container styled as a FloatingActionButton: 56×56, circular borderRadius, Theme.primary background color, elevation 8. Inside the Container, add an Icon widget set to Icons.help_outline, color white, size 28. On the Container's On Tap action, add Show Bottom Sheet and select SupportMenu as the Component to display. Set the Bottom Sheet to half-screen height with rounded top corners (borderRadius top-left and top-right: 16).
Expected result: A circular help button floats in the bottom-right corner of the page. Tapping it opens a Bottom Sheet from the bottom of the screen.
Build the SupportMenu Component with three tappable option cards
Build the SupportMenu Component with three tappable option cards
Create a new Component called SupportMenu. Inside, add a Column with padding 24. First child: a Text widget 'How can we help?' styled as headlineSmall. Below that, add three Container widgets (one per option), each with padding 16, margin bottom 12, borderRadius 12, and a light grey background. Inside each Container, place a Row: an Icon on the left (Icons.search for FAQ, Icons.confirmation_number for Ticket, Icons.chat_bubble for Live Chat), then a SizedBox width 16, then a Column (crossAxisAlignment: start) with a title Text (bodyLarge, bold) and a subtitle Text (bodySmall, secondary color). Titles: 'Search FAQ' / 'Submit a Ticket' / 'Live Chat'. Subtitles: 'Find answers instantly' / 'We will get back to you' / 'Chat with us now'. On Tap for each card: first close the current Bottom Sheet (Navigator.pop), then open a new Bottom Sheet with the specific feature Component (FAQSearch, TicketForm, or LiveChat).
Expected result: The Bottom Sheet displays three clearly labeled option cards. Tapping one closes the menu and opens the selected feature.
Build the FAQ search with Firestore filtering and accordion expand
Build the FAQ search with Firestore filtering and accordion expand
Create a Firestore collection called faq with fields: question (String), answer (String), category (String — 'Getting Started', 'Account', 'Payments', 'Technical'), searchKeywords (Array of Strings — lowercase keywords from the question), and order (Integer for manual sorting). Populate with 10-15 test FAQ entries. Create a FAQSearch Component with a Column: first a TextField with prefixIcon Icons.search, hintText 'Search for help...', and bind its value to a Component State variable called searchQuery. Below the TextField, add ChoiceChips with options Getting Started, Account, Payments, Technical — bind selected value to a Component State variable called selectedCategory. Below that, add a ListView with a Backend Query on the faq collection. When searchQuery is not empty, filter using arrayContainsAny on the searchKeywords field — split the user's searchQuery by spaces into an array using a Custom Function (searchQuery.toLowerCase().split(' ').where((s) => s.isNotEmpty).toList()). When selectedCategory is set, add a where clause: category == selectedCategory. Order by the order field. Each list item is an expandable accordion: a Container with the question as a Text widget and a chevron Icon. On Tap, toggle a local Component State boolean for that item (use the document ID as key). When expanded, show the answer Text below with padding. Use Conditional Visibility on the answer container bound to the expanded state.
Expected result: Users can type keywords to filter FAQ entries and tap category chips to narrow results. Tapping a question expands to reveal the answer.
Build the ticket submission form with Firestore write and Cloud Function email
Build the ticket submission form with Firestore write and Cloud Function email
Create an Option Set called TicketPriority with values: Low, Medium, High, Urgent. Create a Firestore collection called support_tickets with fields: userId (String), subject (String), priority (String), description (String), status (String, default 'new'), createdAt (Timestamp). Build a TicketForm Component with a Column: a Text heading 'Submit a Ticket', a TextField for Subject (bind to Component State subjectText, validator: required), a DropDown bound to the TicketPriority Option Set (default: Medium), a TextField for Description (maxLines: 5, maxLength: 1000, bind to Component State descriptionText, validator: required), and a Submit Button. Add Conditional Visibility or enabled state on the Submit Button: only enable when subjectText is not empty AND descriptionText is not empty. On Tap of Submit: Create Document in support_tickets with userId = currentUser.uid, subject = subjectText, priority = selected priority, description = descriptionText, status = 'new', createdAt = now. On success: show Snackbar 'Ticket submitted! We will get back to you soon.' and close the Bottom Sheet. Deploy a Cloud Function triggered by onCreate on support_tickets/{docId}: read the new document fields, send an email to support@yourapp.com via the SendGrid API with the ticket details (subject, priority, description, user email).
1// Cloud Function: onNewSupportTicket2const functions = require('firebase-functions');3const sgMail = require('@sendgrid/mail');4sgMail.setApiKey(functions.config().sendgrid.key);56exports.onNewSupportTicket = functions.firestore7 .document('support_tickets/{ticketId}')8 .onCreate(async (snap, context) => {9 const ticket = snap.data();10 const msg = {11 to: 'support@yourapp.com',12 from: 'noreply@yourapp.com',13 subject: `[${ticket.priority}] ${ticket.subject}`,14 text: `New support ticket from ${ticket.userId}:\n\nPriority: ${ticket.priority}\nSubject: ${ticket.subject}\nDescription: ${ticket.description}\n\nSubmitted: ${ticket.createdAt.toDate().toISOString()}`,15 };16 await sgMail.send(msg);17 });Expected result: Users fill out the form, tap Submit (only enabled when fields are filled), a Firestore document is created, a Snackbar confirms submission, and your support team receives an email with the ticket details.
Add live chat via Intercom WebView Custom Widget with lazy loading
Add live chat via Intercom WebView Custom Widget with lazy loading
Create a Custom Widget called IntercomChat. The widget accepts parameters: intercomUrl (String — your Intercom Messenger URL), userName (String), and userEmail (String). Inside the Custom Widget Dart code, return a WebView widget (from webview_flutter package) that loads the intercomUrl with query parameters for user identity: '${intercomUrl}?name=${userName}&email=${userEmail}'. CRITICAL: do NOT place this Custom Widget directly on the page or inside a hidden container. Instead, create a LiveChat Component that contains a Column with a header ('Live Chat') and a Container below it. Use Conditional Visibility on the Container: only show when a Component State boolean chatInitialized is true. When the LiveChat Bottom Sheet opens, set chatInitialized to true via an On Page Load or On Component Load action. This way the WebView only initializes when the user actually opens the Live Chat sheet — it never loads in the background. The WebView should fill the available height (use Expanded in the Column). Add a loading indicator that shows while the WebView is loading (use the WebView's onPageStarted and onPageFinished callbacks in the Custom Widget to toggle a loading state).
1// Custom Widget: IntercomChat2import 'package:webview_flutter/webview_flutter.dart';34class IntercomChat extends StatefulWidget {5 final String intercomUrl;6 final String userName;7 final String userEmail;8 final double? width;9 final double? height;1011 const IntercomChat({12 super.key,13 required this.intercomUrl,14 required this.userName,15 required this.userEmail,16 this.width,17 this.height,18 });1920 @override21 State<IntercomChat> createState() => _IntercomChatState();22}2324class _IntercomChatState extends State<IntercomChat> {25 late final WebViewController _controller;26 bool _isLoading = true;2728 @override29 void initState() {30 super.initState();31 final uri = Uri.parse(widget.intercomUrl).replace(32 queryParameters: {33 'name': widget.userName,34 'email': widget.userEmail,35 },36 );37 _controller = WebViewController()38 ..setJavaScriptMode(JavaScriptMode.unrestricted)39 ..setNavigationDelegate(NavigationDelegate(40 onPageFinished: (_) => setState(() => _isLoading = false),41 ))42 ..loadRequest(uri);43 }4445 @override46 Widget build(BuildContext context) {47 return SizedBox(48 width: widget.width,49 height: widget.height,50 child: Stack(51 children: [52 WebViewWidget(controller: _controller),53 if (_isLoading)54 const Center(child: CircularProgressIndicator()),55 ],56 ),57 );58 }59}Expected result: Tapping Live Chat opens a Bottom Sheet that initializes the Intercom WebView on demand. A loading spinner shows while the page loads. The user can chat with your support team directly inside the app.
Complete working example
1Firestore Data Model:2├── faq/{docId}3│ ├── question: String ("How do I reset my password?")4│ ├── answer: String ("Go to Settings > Account > Reset Password...")5│ ├── category: String ("Account")6│ ├── searchKeywords: Array ["how", "reset", "password"]7│ └── order: Integer (1)8└── support_tickets/{docId}9 ├── userId: String (auth UID)10 ├── subject: String ("Cannot access dashboard")11 ├── priority: String ("High")12 ├── description: String ("When I click...")13 ├── status: String ("new")14 └── createdAt: Timestamp1516Option Set:17└── TicketPriority: Low | Medium | High | Urgent1819Page Structure (any page):20└── Stack21 ├── [Existing page content]22 └── Align (bottomRight, padding: 16)23 └── Container (56x56, circular, Theme.primary, elevation: 8)24 └── Icon (Icons.help_outline, white, 28)25 └── On Tap → Show Bottom Sheet: SupportMenu2627SupportMenu Component (Bottom Sheet):28└── Column (padding: 24)29 ├── Text ("How can we help?", headlineSmall)30 ├── OptionCard (Icons.search, "Search FAQ", "Find answers instantly")31 │ └── On Tap → Close Sheet → Show Bottom Sheet: FAQSearch32 ├── OptionCard (Icons.confirmation_number, "Submit a Ticket", "We'll get back to you")33 │ └── On Tap → Close Sheet → Show Bottom Sheet: TicketForm34 └── OptionCard (Icons.chat_bubble, "Live Chat", "Chat with us now")35 └── On Tap → Close Sheet → Show Bottom Sheet: LiveChat3637FAQSearch Component (Bottom Sheet):38└── Column39 ├── TextField (Icons.search prefix, "Search for help...")40 │ └── Bound to Component State: searchQuery41 ├── ChoiceChips (Getting Started | Account | Payments | Technical)42 │ └── Bound to Component State: selectedCategory43 └── ListView (Backend Query: faq, filtered by searchKeywords + category)44 └── Expandable Item45 ├── Row (question Text + chevron Icon)46 └── Conditional: answer Text (visible when expanded)4748TicketForm Component (Bottom Sheet):49└── Column50 ├── Text ("Submit a Ticket", headlineSmall)51 ├── TextField (Subject, required)52 ├── DropDown (TicketPriority Option Set, default: Medium)53 ├── TextField (Description, maxLines: 5, maxLength: 1000)54 └── Button ("Submit", enabled when subject + description non-empty)55 └── On Tap → Create Doc (support_tickets) → Snackbar → Close Sheet5657LiveChat Component (Bottom Sheet):58└── Column59 ├── Text ("Live Chat", headlineSmall)60 └── IntercomChat Custom Widget (lazy: only created when sheet opens)61 └── WebView loading Intercom URL with user name + email params6263Cloud Function:64└── onNewSupportTicket (Firestore onCreate: support_tickets/{docId})65 └── Send email via SendGrid to support@yourapp.comCommon mistakes when creating a Custom Support Widget for Your FlutterFlow App
Why it's a problem: Initializing the Intercom WebView eagerly on page load
How to avoid: Only create the IntercomChat Custom Widget when the user actually taps Live Chat. Place it inside a Component that is only loaded as a Bottom Sheet on demand — the WebView instance does not exist until that sheet opens.
Why it's a problem: Not splitting search terms before querying Firestore with arrayContainsAny
How to avoid: Use a Custom Function to split the user's search input by spaces into an array of lowercase strings: searchQuery.toLowerCase().split(' ').where((s) => s.isNotEmpty).toList(). Pass this list to arrayContainsAny.
Why it's a problem: Not validating required fields on the ticket submission form
How to avoid: Disable the Submit button until both the subject TextField and description TextField contain non-empty text. Bind the button's enabled state to a condition: subjectText.isNotEmpty AND descriptionText.isNotEmpty.
Best practices
- Use a page-level Stack for the FAB so it floats above scrollable content without interfering with the layout
- Keep the searchKeywords array as individual lowercase words — never store phrases or sentences as keywords
- Limit Firestore arrayContainsAny to 10 values (Firestore limit) — truncate the split search terms if the user types more than 10 words
- Set maxLength on the description TextField (1000 chars) to prevent abuse and keep Firestore document sizes small
- Add a loading indicator inside the WebView Custom Widget so users see feedback while Intercom loads
- Use an Option Set for ticket priority instead of hardcoded strings — easier to update and type-safe in queries
- Close the current Bottom Sheet before opening the next one to avoid stacking sheets on top of each other
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Design a Firestore data model for an in-app support widget with three features: FAQ search (collection with question, answer, category, searchKeywords array), ticket submission (collection with userId, subject, priority, description, status, timestamp), and a Cloud Function that emails the support team via SendGrid when a new ticket is created. Give me the Firestore security rules for both collections.
Create a Bottom Sheet Component with three option cards in a Column: Search FAQ (search icon), Submit a Ticket (ticket icon), and Live Chat (chat icon). Each card is a Container with a Row containing an Icon, a title Text, and a subtitle Text. Style with rounded corners, light grey background, and padding 16.
Frequently asked questions
Can I use Crisp or Zendesk instead of Intercom for live chat?
Yes. The same WebView Custom Widget approach works with any chat provider that offers a web-based messenger URL. Replace the intercomUrl parameter with your Crisp or Zendesk messenger URL. The lazy-loading pattern is identical — only the URL changes.
How do I let users track their ticket status after submitting?
Add a 'My Tickets' page or section that queries the support_tickets collection filtered by userId == currentUser.uid, ordered by createdAt descending. Display each ticket's subject, priority, status, and timestamp. Update the status field from your admin dashboard or a Cloud Function when the ticket is resolved.
Does the Intercom WebView work on mobile and web?
The WebView Custom Widget works on iOS and Android builds. For FlutterFlow web builds, WebView is not supported — instead use Intercom's JavaScript snippet injected via a Custom Widget that renders an HtmlElementView with the Intercom script tag.
How do I make the FAQ search work with partial word matches?
Firestore arrayContainsAny only matches exact strings. For partial matches, add multiple keyword variants to the searchKeywords array (e.g., 'pay', 'payment', 'payments'). For true full-text search, integrate Algolia or Typesense and query via an API call instead of a direct Firestore query.
Can I add the support FAB to every page without duplicating it?
Yes. Wrap your app's main navigation in a global Stack (in your NavBar page or root layout) and place the FAB there once. It will appear on every page that uses that navigation structure. Alternatively, create the FAB as a reusable Component and drop it into each page's Stack.
What if I do not need all three options — can I show just FAQ and tickets?
Absolutely. Remove the Live Chat option card from the SupportMenu Component and delete the LiveChat Component and IntercomChat Custom Widget. The SupportMenu works with any number of options — just remove the ones you do not need.
Can RapidDev help build a production support system with routing and SLA tracking?
Yes. A production support system with ticket routing, SLA timers, agent assignment, priority escalation, and analytics dashboards requires Cloud Functions and admin tooling beyond the visual builder. RapidDev can architect and build the complete system.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation