Build a dismissible promotional banner Component at the top of any page using a gradient Container with title Text, CTA Button, and close IconButton. Add a timed popup promo modal that appears after a 5-second delay via On Page Load Wait action. Pull all promo content from a Firestore promotions collection with fields for title, body, ctaUrl, isActive, startDate, endDate, and variant (A/B testing). Persist banner dismissal in App State so it does not reappear for the same promotion.
Building Dismissible Promo Banners and Timed Popup Modals in FlutterFlow
In-app promotions drive engagement and revenue, but they must be manageable without code deploys. This tutorial builds a Firestore-driven banner and popup system where your marketing team controls content, scheduling, and A/B variants from the Firebase console.
Prerequisites
- A FlutterFlow project with Firestore configured
- A promotions collection in Firestore with at least one test document
- Basic familiarity with App State and Conditional Visibility
Step-by-step guide
Create the Firestore promotions collection with scheduling and A/B variant fields
Create the Firestore promotions collection with scheduling and A/B variant fields
In Firestore, create a promotions collection. Each document needs: title (String), body (String), ctaText (String), ctaUrl (String), imageUrl (String, optional), isActive (Boolean), startDate (Timestamp), endDate (Timestamp), variant (String — 'A' or 'B' for A/B testing), type (String — 'banner' or 'popup'). Add a composite index on isActive + startDate for efficient queries. Create one test document with type: 'banner' and one with type: 'popup'.
Expected result: Firestore has a promotions collection with scheduling fields and two test promo documents.
Build the PromoBanner Component with gradient Container, CTA Button, and close IconButton
Build the PromoBanner Component with gradient Container, CTA Button, and close IconButton
Create a Component named PromoBanner with parameters: title (String), body (String), ctaText (String), ctaUrl (String), promoId (String). The root widget is a Container with gradient background (e.g., primary → secondary), horizontal padding 16, vertical padding 12. Inside, a Row: Expanded Column (title Text in white bold, body Text in white70) + Button (ctaText, white background, primary text color, On Tap → Launch URL ctaUrl) + IconButton (close_rounded, white, On Tap → dismiss logic described in step 4).
Expected result: A gradient banner Component renders with promotional text, a CTA button, and a close button.
Query active promotions with date filtering and display the banner conditionally
Query active promotions with date filtering and display the banner conditionally
On the home page, add a Backend Query on promotions where isActive == true AND type == 'banner' AND startDate <= now AND endDate >= now, limit 1. Bind the PromoBanner Component to the query result at the top of the page Column. Wrap the PromoBanner in Conditional Visibility: visible if the query returns a result AND the promoId is NOT in the App State dismissedPromoIds list.
Expected result: The banner appears at the top of the home page only when an active, current-date promotion exists and has not been dismissed.
Persist banner dismissal in App State dismissedPromoIds list
Persist banner dismissal in App State dismissedPromoIds list
Create an App State variable dismissedPromoIds (String List, persisted). On the PromoBanner close IconButton tap: add the current promoId to dismissedPromoIds via Update App State → Add to List. The Conditional Visibility check (promoId NOT in dismissedPromoIds) hides the banner immediately. Because the list is persisted, dismissed banners stay hidden across app restarts.
Expected result: Tapping the close button hides the banner permanently for that specific promotion, even after app restart.
Add a timed popup promo modal triggered 5 seconds after page load
Add a timed popup promo modal triggered 5 seconds after page load
On the home page On Page Load action, add: Backend Query promotions where isActive == true AND type == 'popup' AND startDate <= now AND endDate >= now, limit 1. If the query returns a result AND promoId is NOT in dismissedPromoIds: Wait 5000ms → Show Dialog with a PromoPopup Component. The PromoPopup has an Image (promo imageUrl), title Text, body Text, CTA Button (Launch URL), and a close X IconButton. On the close IconButton or CTA tap, add promoId to dismissedPromoIds and close the dialog.
Expected result: Five seconds after page load, a promo popup dialog appears with image, text, and CTA. Dismissing it prevents it from showing again.
Complete working example
1FIRESTORE DATA MODEL:2 promotions/{promoId}3 title: String ("Spring Sale — 30% Off")4 body: String ("Use code SPRING30 at checkout")5 ctaText: String ("Shop Now")6 ctaUrl: String ("https://example.com/sale")7 imageUrl: String (optional, for popup hero image)8 isActive: Boolean9 startDate: Timestamp10 endDate: Timestamp11 variant: String ("A" | "B")12 type: String ("banner" | "popup")13 Index: isActive ASC, startDate ASC1415APP STATE:16 dismissedPromoIds: String List (persisted)1718PROMO BANNER COMPONENT:19 Parameters: title, body, ctaText, ctaUrl, promoId20 Container (gradient: primary → secondary, borderRadius: 8, padding: 12 16)21 Row22 ├── Expanded Column23 │ ├── Text (title, bodyLarge, white, bold)24 │ └── Text (body, bodySmall, white70)25 ├── Button (ctaText, white bg, primary text)26 │ On Tap → Launch URL ctaUrl27 └── IconButton (close_rounded, white)28 On Tap → Add promoId to App State dismissedPromoIds2930PROMO POPUP COMPONENT:31 Parameters: title, body, ctaText, ctaUrl, imageUrl, promoId32 Container (borderRadius: 16, padding: 24, white bg)33 Column (center)34 ├── Align (topRight) → IconButton (close, grey)35 │ On Tap → Add promoId to dismissedPromoIds → Close Dialog36 ├── Image (imageUrl, height: 200, fit: cover, borderRadius: 12)37 ├── SizedBox (height: 16)38 ├── Text (title, headlineSmall, bold)39 ├── Text (body, bodyMedium, grey)40 ├── SizedBox (height: 16)41 └── Button (ctaText, full width, primary)42 On Tap → Launch URL ctaUrl → Add promoId to dismissedPromoIds → Close Dialog4344HOME PAGE ON PAGE LOAD:45 1. Query promotions (isActive, type: banner, date range) → show PromoBanner if not dismissed46 2. Query promotions (isActive, type: popup, date range) → if not dismissed → Wait 5s → Show Dialog PromoPopupCommon mistakes when creating a Custom Marketing Widget for Your FlutterFlow App
Why it's a problem: Not adding startDate and endDate filters to the promotions query
How to avoid: Always filter where startDate <= now AND endDate >= now in the Backend Query so only current promotions display.
Why it's a problem: Storing dismissed state only in Page State instead of persisted App State
How to avoid: Use an App State String List with Persisted enabled. Dismissed promoIds survive page navigation and app restarts.
Why it's a problem: Showing the popup immediately on page load without a delay
How to avoid: Add a Wait action (5000ms) before Show Dialog so users have time to settle into the page before seeing the promotion.
Best practices
- Store all promo content in Firestore so the marketing team can update without code changes
- Use startDate and endDate fields for automated scheduling — no manual activation/deactivation needed
- Add a variant field (A/B) and randomly assign users to test which promo performs better
- Persist dismissed promo IDs in App State so users are not shown the same promo twice
- Limit popup frequency to one per session — check a sessionPopupShown Page State before showing
- Use gradient backgrounds on banners for visual prominence without requiring a designer
- Include the promo type field (banner vs popup) to control display format from Firestore
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to build a Firestore-driven promotional banner and timed popup in FlutterFlow. Show me the Firestore data model with scheduling fields, the banner Component with dismiss persistence, and the On Page Load popup trigger with a 5-second delay.
Create a gradient banner at the top of the home page with a title, description, action button, and close button. Also create a popup dialog that appears 5 seconds after the page loads with an image, text, and call-to-action button.
Frequently asked questions
How do I implement A/B testing for promotional content?
Add a variant field (A or B) to each promotion document. On the client, randomly assign the user a variant on first launch (store in App State). Filter the promotions query to match the user's variant so they consistently see the same version.
Can the marketing team schedule promotions in advance?
Yes. The startDate and endDate fields on each promotion document control when it appears. Set these dates in the Firebase console. The app query filters by current date automatically.
How do I prevent the popup from showing on every page visit?
When the user dismisses or clicks the CTA, add the promoId to the persisted App State dismissedPromoIds list. The Conditional Visibility check prevents it from appearing again.
Can I show different banners on different pages?
Yes. Add a page field (String) to the promotion document specifying which page it targets. Filter the query on each page to match its own page identifier.
How do I track how many users click the CTA button?
On the CTA tap action, before launching the URL, add a Firestore increment on the promotion document's clickCount field using FieldValue.increment(1) in a Custom Action.
Can RapidDev help build a full in-app marketing and analytics system?
Yes. RapidDev can implement targeted promotions with user segmentation, impression and click analytics, push notification campaigns, and an admin dashboard for managing all marketing content.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation