# How to Create a Custom Marketing Widget for Your FlutterFlow App

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 20-25 min
- Compatibility: FlutterFlow Free+
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

### 2. 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.

### 3. 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.

### 4. 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.

### 5. 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 code example

File: `FlutterFlow Marketing Widget Setup`

```text
FIRESTORE DATA MODEL:
  promotions/{promoId}
    title: String ("Spring Sale — 30% Off")
    body: String ("Use code SPRING30 at checkout")
    ctaText: String ("Shop Now")
    ctaUrl: String ("https://example.com/sale")
    imageUrl: String (optional, for popup hero image)
    isActive: Boolean
    startDate: Timestamp
    endDate: Timestamp
    variant: String ("A" | "B")
    type: String ("banner" | "popup")
  Index: isActive ASC, startDate ASC

APP STATE:
  dismissedPromoIds: String List (persisted)

PROMO BANNER COMPONENT:
  Parameters: title, body, ctaText, ctaUrl, promoId
  Container (gradient: primary → secondary, borderRadius: 8, padding: 12 16)
    Row
      ├── Expanded Column
      │     ├── Text (title, bodyLarge, white, bold)
      │     └── Text (body, bodySmall, white70)
      ├── Button (ctaText, white bg, primary text)
      │     On Tap → Launch URL ctaUrl
      └── IconButton (close_rounded, white)
            On Tap → Add promoId to App State dismissedPromoIds

PROMO POPUP COMPONENT:
  Parameters: title, body, ctaText, ctaUrl, imageUrl, promoId
  Container (borderRadius: 16, padding: 24, white bg)
    Column (center)
      ├── Align (topRight) → IconButton (close, grey)
      │     On Tap → Add promoId to dismissedPromoIds → Close Dialog
      ├── Image (imageUrl, height: 200, fit: cover, borderRadius: 12)
      ├── SizedBox (height: 16)
      ├── Text (title, headlineSmall, bold)
      ├── Text (body, bodyMedium, grey)
      ├── SizedBox (height: 16)
      └── Button (ctaText, full width, primary)
            On Tap → Launch URL ctaUrl → Add promoId to dismissedPromoIds → Close Dialog

HOME PAGE ON PAGE LOAD:
  1. Query promotions (isActive, type: banner, date range) → show PromoBanner if not dismissed
  2. Query promotions (isActive, type: popup, date range) → if not dismissed → Wait 5s → Show Dialog PromoPopup
```

## Common mistakes

- **Not adding startDate and endDate filters to the promotions query** — Expired promotions continue showing indefinitely, confusing users with outdated deals and broken links. Fix: Always filter where startDate <= now AND endDate >= now in the Backend Query so only current promotions display.
- **Storing dismissed state only in Page State instead of persisted App State** — Page State resets on navigation. Users see the same dismissed banner again every time they visit the page. Fix: Use an App State String List with Persisted enabled. Dismissed promoIds survive page navigation and app restarts.
- **Showing the popup immediately on page load without a delay** — An instant popup feels aggressive and interrupts the user before they have engaged with the page content. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-marketing-widget-for-your-flutterflow-app
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-marketing-widget-for-your-flutterflow-app
