> ## Documentation Index
> Fetch the complete documentation index at: https://docs.presentum.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Popup hosts

> Advanced dialog and overlay management with PresentumPopupSurfaceStateMixin

## What are popup hosts?

Popup hosts are widgets that automatically show and dismiss dialogs, fullscreen overlays, or modal presentations based on Presentum surface state. They build on top of surface observers to add popup-specific features:

* **Duplicate detection** - Prevent showing the same popup twice in quick succession
* **Conflict resolution** - Configure what happens when popups overlap
* **Automatic queuing** - Queue popups to show sequentially
* **Smart dismissal tracking** - Only track dismissals for system-closed popups

<Tip>
  Use popup hosts for dialogs, fullscreen pages, bottom sheets, and any modal UI
  that requires user interaction and navigation integration.
</Tip>

## Prerequisites

The `PresentumPopupSurfaceStateMixin` requires `PresentumActiveSurfaceItemObserverMixin` as a base. If you haven't used surface observers before, read the [Surface observers guide](/advanced/surface-observers) first.

<Warning>
  Both mixins are required. You must add
  `PresentumActiveSurfaceItemObserverMixin` before
  `PresentumPopupSurfaceStateMixin` in your mixin list.
</Warning>

## Basic usage

Here's a minimal popup host:

```dart theme={null}
class CampaignPopupHost extends StatefulWidget {
  const CampaignPopupHost({required this.child, super.key});

  final Widget child;

  @override
  State<CampaignPopupHost> createState() => _CampaignPopupHostState();
}

class _CampaignPopupHostState extends State<CampaignPopupHost>
    with
        PresentumActiveSurfaceItemObserverMixin<
          CampaignItem,
          CampaignSurface,
          CampaignVariant,
          CampaignPopupHost
        >,
        PresentumPopupSurfaceStateMixin<
          CampaignItem,
          CampaignSurface,
          CampaignVariant,
          CampaignPopupHost
        > {

  @override
  CampaignSurface get surface => CampaignSurface.popup;

  @override
  Future<PopupPresentResult> present(CampaignItem entry) async {
    if (!mounted) return PopupPresentResult.notPresented;

    // Record impression
    final campaigns = context.campaignsPresentum;
    await campaigns.markShown(entry);

    if (!mounted) return PopupPresentResult.notPresented;

    // Show dialog
    final result = await showDialog<bool?>(
      context: context,
      builder: (context) => InheritedPresentum.value(
        value: campaigns,
        child: InheritedPresentumItem(
          item: entry,
          child: CampaignDialog(),
        ),
      ),
      barrierDismissible: false,
    );

    // Map to PopupPresentResult
    return result == true
        ? PopupPresentResult.userDismissed
        : PopupPresentResult.systemDismissed;
  }

  @override
  Widget build(BuildContext context) => widget.child;
}
```

## Required implementations

You must implement two members:

```dart theme={null}
@override
PresentumSurface get surface; // Which surface to observe

@override
Future<PopupPresentResult> present(TItem entry); // Show the popup
```

### PopupPresentResult values

Return the appropriate enum value based on how the popup was dismissed:

* **`PopupPresentResult.userDismissed`** - User took action (e.g., clicked "Buy Now", converted). The mixin will NOT call `markDismissed()` because you should have already handled it.

* **`PopupPresentResult.systemDismissed`** - User closed the popup without taking action (e.g., clicked "X" or "Dismiss"). The mixin WILL call `markDismissed()` automatically.

* **`PopupPresentResult.notPresented`** - Could not show the popup (e.g., widget not mounted). The mixin will NOT call `markDismissed()`.

<Info>
  The mixin provides a default `markDismissed()` implementation that delegates
  to `context.presentum().markDismissed()`. Override only if you need custom
  dismissal logic.
</Info>

## What the mixin provides

The `PresentumPopupSurfaceStateMixin` automatically:

1. **Observes surface state** via the required observer mixin
2. **Shows popups** when active item changes
3. **Dismisses popups** when active item becomes null
4. **Prevents duplicates** with optional duplicate detection
5. **Resolves conflicts** when multiple popups activate
6. **Manages queue** for sequential popup display
7. **Tracks dismissals** intelligently based on `PopupPresentResult`

## Optional configuration

### Duplicate detection

Prevent showing the same popup multiple times in quick succession:

```dart theme={null}
class _CampaignPopupHostState extends State<CampaignPopupHost>
    with
        PresentumActiveSurfaceItemObserverMixin<...>,
        PresentumPopupSurfaceStateMixin<...> {

  @override
  bool get ignoreDuplicates => true;

  @override
  Duration? get duplicateThreshold => const Duration(seconds: 5);

  // ... rest of implementation
}
```

* **`ignoreDuplicates`**: Enable/disable duplicate detection (default: `false`)
* **`duplicateThreshold`**: Time window for considering entries duplicates
  * If `null`, always ignore duplicates of the same ID
  * If set, only ignore if shown within the threshold

### Conflict strategies

Control what happens when a new popup activates while another is showing:

#### Ignore (default)

Keep showing the current popup, ignore new ones:

```dart theme={null}
@override
PopupConflictStrategy get conflictStrategy => PopupConflictStrategy.ignore;
```

**Use when:** You want users to finish with one popup before seeing another.

#### Replace

Immediately dismiss the current popup and show the new one:

```dart theme={null}
@override
PopupConflictStrategy get conflictStrategy => PopupConflictStrategy.replace;
```

**Use when:** Newer popups have higher priority than older ones.

#### Queue

Queue new popups to show after the current one is dismissed:

```dart theme={null}
@override
PopupConflictStrategy get conflictStrategy => PopupConflictStrategy.queue;
```

**Use when:** You want to show all popups sequentially without skipping any.

### Queuing behavior example

When using `PopupConflictStrategy.queue`:

```
State: popup surface has item A showing, items B and C activate

With conflictStrategy = queue:
├─ active: Campaign A (showing)
└─ queue: [Campaign B, Campaign C]

User flow:
1. Campaign A dialog shows
2. User dismisses A
3. Campaign B dialog shows automatically (from queue)
4. User dismisses B
5. Campaign C dialog shows automatically (from queue)
```

With other strategies:

* **`ignore`**: Items B and C are ignored while A shows
* **`replace`**: A is dismissed, B shows immediately (then C replaces B)

## Production example

This is the actual popup host from the example app, managing campaign dialogs and fullscreen promos:

```dart theme={null}
class CampaignPopupHost extends StatefulWidget {
  const CampaignPopupHost({required this.child, super.key});

  final Widget child;

  @override
  State<CampaignPopupHost> createState() => _CampaignPopupHostState();
}

class _CampaignPopupHostState extends State<CampaignPopupHost>
    with
        PresentumActiveSurfaceItemObserverMixin<
          CampaignPresentumItem,
          CampaignSurface,
          CampaignVariant,
          CampaignPopupHost
        >,
        PresentumPopupSurfaceStateMixin<
          CampaignPresentumItem,
          CampaignSurface,
          CampaignVariant,
          CampaignPopupHost
        > {
  @override
  CampaignSurface get surface => CampaignSurface.popup;

  @override
  bool get ignoreDuplicates => true;

  @override
  Future<PopupPresentResult> present(CampaignPresentumItem entry) async {
    if (!mounted) return PopupPresentResult.notPresented;

    final campaigns = context.campaignsPresentum;
    await campaigns.markShown(entry);

    if (!mounted) return PopupPresentResult.notPresented;

    final factory = campaignsPresentationWidgetFactory;
    final fullscreenDialog =
        entry.option.variant == CampaignVariant.fullscreenDialog;

    final result = await showDialog<bool?>(
      context: context,
      builder: (context) => InheritedPresentum.value(
        value: campaigns,
        child: InheritedPresentumItem<
          CampaignPresentumItem,
          CampaignSurface,
          CampaignVariant
        >(
          item: entry,
          child: factory.buildPopup(context, entry),
        ),
      ),
      barrierDismissible: false,
      fullscreenDialog: fullscreenDialog,
    );

    return result == true
        ? PopupPresentResult.userDismissed
        : PopupPresentResult.systemDismissed;
  }

  @override
  Widget build(BuildContext context) => widget.child;
}
```

[See full source →](https://github.com/itsezlife/presentum/blob/master/example/lib/src/campaigns/presentum/widgets/campaign_popup_host.dart)

<Note>
  Notice `markDismissed` is not implemented. The default implementation handles
  it automatically.
</Note>

## Custom presentation

### Fullscreen routes

Use `Navigator.push` instead of `showDialog` for fullscreen presentations:

```dart theme={null}
@override
Future<PopupPresentResult> present(CampaignItem entry) async {
  if (!mounted) return PopupPresentResult.notPresented;

  final presentum = context.presentum<CampaignItem, AppSurface, CampaignVariant>();
  await presentum.markShown(entry);

  if (!mounted) return PopupPresentResult.notPresented;

  final result = await Navigator.of(context).push<bool>(
    MaterialPageRoute(
      builder: (context) => InheritedPresentum.value(
        value: presentum,
        child: InheritedPresentumItem(
          item: entry,
          child: FullscreenCampaignPage(),
        ),
      ),
      fullscreenDialog: true,
    ),
  );

  return result == true
      ? PopupPresentResult.userDismissed
      : PopupPresentResult.systemDismissed;
}
```

### Bottom sheets

Use `showModalBottomSheet` for bottom sheet presentations:

```dart theme={null}
@override
Future<PopupPresentResult> present(CampaignItem entry) async {
  if (!mounted) return PopupPresentResult.notPresented;

  final presentum = context.presentum<CampaignItem, AppSurface, CampaignVariant>();
  await presentum.markShown(entry);

  if (!mounted) return PopupPresentResult.notPresented;

  final result = await showModalBottomSheet<bool>(
    context: context,
    builder: (context) => InheritedPresentumItem(
      item: entry,
      child: CampaignBottomSheet(),
    ),
    isDismissible: false,
  );

  return result == true
      ? PopupPresentResult.userDismissed
      : PopupPresentResult.systemDismissed;
}
```

### Custom pop behavior

Override `pop()` for custom navigation:

```dart theme={null}
@override
void pop() {
  if (mounted) {
    // Use a custom navigator or close logic
    MyCustomNavigator.of(context).pop();
  }
}
```

## Dialog widgets

Create dialog widgets that access the item via `InheritedPresentumItem`:

```dart theme={null}
class CampaignDialog extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final item = context.presentumItem<
      CampaignItem,
      CampaignSurface,
      CampaignVariant
    >();

    return Dialog(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              item.metadata['title'] as String,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 16),
            Text(item.metadata['message'] as String),
            const SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () => Navigator.pop(context, false),
                  child: const Text('Dismiss'),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: () async {
                    final campaigns = context.presentum<
                      CampaignItem,
                      CampaignSurface,
                      CampaignVariant
                    >();
                    await campaigns.markConverted(item);
                    if (context.mounted) {
                      Navigator.pop(context, true);
                    }
                  },
                  child: const Text('Take Action'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
```

## Integration

Initialize the presentum and wrap your app with the popup host:

```dart theme={null}
class CampaignsPresentum extends StatefulWidget {
  const CampaignsPresentum({required this.child, super.key});

  final Widget child;

  @override
  State<CampaignsPresentum> createState() => _CampaignsPresentumState();
}

class _CampaignsPresentumState extends State<CampaignsPresentum>
    with CampaignsPresentumStateMixin {
  @override
  Widget build(BuildContext context) {
    return campaignsPresentum.config.engine.build(
      context,
      CampaignsProviderScope(
        provider: provider,
        child: CampaignPopupHost(
          child: widget.child,
        ),
      ),
    );
  }
}
```

Then use it in your app:

```dart theme={null}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CampaignsPresentum(
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}
```

The host watches for popup surface changes and shows dialogs automatically.

<Info>
  This pattern initializes the presentum engine, wraps the child with the
  provider scope for context access, and adds the popup host for automatic
  dialog management.
</Info>

## Best practices

<AccordionGroup>
  <Accordion title="Always wrap with InheritedPresentumItem">
    This ensures descendants can access the item and presentum instance:

    ```dart theme={null}
    // ✅ Good
    builder: (context) => InheritedPresentum.value(
      value: presentum,
      child: InheritedPresentumItem(
        item: entry,
        child: MyDialog(),
      ),
    )

    // ❌ Bad - descendants can't access item
    builder: (context) => MyDialog()
    ```
  </Accordion>

  <Accordion title="Record impressions before showing">
    Call `markShown` before displaying the popup:

    ```dart theme={null}
    // ✅ Good
    await presentum.markShown(entry);
    await showDialog(...);

    // ❌ Bad - impression not recorded
    await showDialog(...);
    ```
  </Accordion>

  <Accordion title="Check mounted before presenting">
    Always check if the widget is mounted before showing popups:

    ```dart theme={null}
    // ✅ Good
    @override
    Future<PopupPresentResult> present(Item entry) async {
      if (!mounted) return PopupPresentResult.notPresented;
      
      await markShown(entry);
      
      if (!mounted) return PopupPresentResult.notPresented;
      
      final result = await showDialog(...);
      return result == true 
        ? PopupPresentResult.userDismissed 
        : PopupPresentResult.systemDismissed;
    }
    ```
  </Accordion>

  <Accordion title="Return correct PopupPresentResult">
    Map dialog results to the appropriate enum value:

    ```dart theme={null}
    // User converted - return userDismissed
    onPressed: () async {
      await presentum.markConverted(item);
      if (context.mounted) {
        Navigator.pop(context, true); // true = user action
      }
    }

    // User just closed - return systemDismissed
    onPressed: () => Navigator.pop(context, false) // false = system close

    // Map in present method
    final result = await showDialog<bool?>(...);
    return result == true
        ? PopupPresentResult.userDismissed // Won't call markDismissed
        : PopupPresentResult.systemDismissed; // Will call markDismissed
    ```

    This ensures dismissal tracking is only recorded for non-conversions.
  </Accordion>

  <Accordion title="Add both mixins in correct order">
    Always add `PresentumActiveSurfaceItemObserverMixin` before `PresentumPopupSurfaceStateMixin`:

    ```dart theme={null}
    // ✅ Good
    class _MyState extends State<MyWidget>
        with
            PresentumActiveSurfaceItemObserverMixin<...>,
            PresentumPopupSurfaceStateMixin<...> { }

    // ❌ Bad - wrong order will cause compilation errors
    class _MyState extends State<MyWidget>
        with
            PresentumPopupSurfaceStateMixin<...>,
            PresentumActiveSurfaceItemObserverMixin<...> { }
    ```
  </Accordion>
</AccordionGroup>

## Advanced: Custom dismissal tracking

Override `markDismissed` for custom logic:

```dart theme={null}
@override
Future<void> markDismissed({required CampaignItem entry}) async {
  // Custom dismissal logic
  await myAnalytics.trackDismissal(entry.id);
  await myStorage.saveDismissedAt(entry.id, DateTime.now());

  // Still call the default if needed
  await super.markDismissed(entry: entry);
}
```

## When to use popup hosts

Use `PresentumPopupSurfaceStateMixin` when you need:

<CardGroup cols={2}>
  <Card title="Dialogs" icon="window">
    Modal dialogs that require user interaction
  </Card>

  {" "}

  <Card title="Fullscreen pages" icon="expand">
    Fullscreen promotional content or onboarding flows
  </Card>

  {" "}

  <Card title="Bottom sheets" icon="rectangle-vertical">
    Modal bottom sheets with actions
  </Card>

  <Card title="Conflict resolution" icon="shuffle">
    Smart handling of overlapping popups with queueing
  </Card>
</CardGroup>

<Warning>
  Don't use popup hosts for non-modal UI like snackbars or banners. Use [Surface
  observers](/advanced/surface-observers) instead for simpler, more appropriate
  handling.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Surface observers" icon="eye" href="/advanced/surface-observers">
    Learn about the underlying observer mixin
  </Card>

  {" "}

  <Card title="Inherited widgets" icon="sitemap" href="/features/inherited-widgets">
    Using InheritedPresentumItem in dialogs
  </Card>

  {" "}

  <Card title="Production example" icon="code" href="https://github.com/itsezlife/presentum/blob/master/example/lib/src/campaigns/presentum/widgets/campaign_popup_host.dart">
    Complete popup host implementation
  </Card>

  <Card title="Example app" icon="rocket" href="https://github.com/itsezlife/presentum/tree/master/example">
    Explore the full example application
  </Card>
</CardGroup>
