Skip to main content

Overview

Presentum uses three related types to represent presentations:
  • Payloads - Your domain data (campaigns, tips, updates)
  • Options - How payloads appear (surface + variant + rules)
  • Items - Concrete decisions (payload + option)

Payloads

Payloads are your domain objects containing all presentation data:
class CampaignPayload extends PresentumPayload<AppSurface, CampaignVariant> {
  const CampaignPayload({
    required this.id,
    required this.priority,
    required this.metadata,
    required this.options,
  });

  @override
  final String id;

  @override
  final int priority;

  @override
  final Map<String, Object?> metadata;

  @override
  final List<PresentumOption<AppSurface, CampaignVariant>> options;
}

Required fields

id
String
required
Unique identifier for the payload. Used for tracking and deduplication.
priority
int
required
Display priority. Higher priority items shown first. Typical range: 0-1000.
metadata
Map<String, Object?>
required
Arbitrary domain data. Store titles, images, URLs, eligibility data, etc.
options
List<PresentumOption>
required
How this payload can be presented across different surfaces.

Production example

From a real app with JSON deserialization:
class CampaignPayload extends PresentumPayload<CampaignSurface, CampaignVariant>
    implements HasMetadata {
  const CampaignPayload({
    required this.id,
    required this.priority,
    required this.metadata,
    required this.options,
  });

  factory CampaignPayload.fromJson(Map<String, Object?> json) {
    return CampaignPayload(
      id: json['id'] as String,
      priority: (json['priority'] as num?)?.toInt() ?? 0,
      metadata: json['metadata'] as Map<String, Object?>,
      options: (json['options'] as List)
          .map((o) => CampaignPresentumOption.fromJson(o))
          .toList(),
    );
  }

  @override
  final String id;

  @override
  final int priority;

  @override
  final Map<String, Object?> metadata;

  @override
  final List<CampaignPresentumOption> options;

  Map<String, Object?> toJson() => {
    'id': id,
    'priority': priority,
    'metadata': metadata,
    'options': options.map((o) => o.toJson()).toList(),
  };
}
See full payload implementation ->

Options

Options describe how a payload appears on a specific surface:
class CampaignPresentumOption
    extends PresentumOption<AppSurface, CampaignVariant> {
  const CampaignPresentumOption({
    required this.surface,
    required this.variant,
    required this.isDismissible,
    this.stage,
    this.maxImpressions,
    this.cooldownMinutes,
    this.alwaysOnIfEligible = false,
  });

  @override
  final AppSurface surface;

  @override
  final CampaignVariant variant;

  @override
  final bool isDismissible;

  @override
  final int? stage;

  @override
  final int? maxImpressions;

  @override
  final int? cooldownMinutes;

  @override
  final bool alwaysOnIfEligible;
}

Option fields

surface
S extends PresentumSurface
required
Where this option appears (e.g., AppSurface.homeTopBanner)
variant
V extends PresentumVisualVariant
required
How this option is displayed (e.g., CampaignVariant.banner)
isDismissible
bool
required
Whether users can close this presentation
stage
int?
Sequence hint for multi-stage flows (e.g., 0 = fullscreen, 1 = dialog)
maxImpressions
int?
Maximum times to show. null = unlimited
cooldownMinutes
int?
Minutes to wait between shows. null = no cooldown
alwaysOnIfEligible
bool
Show immediately when eligible without requiring explicit activation

Multi-surface payloads

One payload can have multiple options for different surfaces:
CampaignPayload(
  id: 'black-friday-2025',
  priority: 100,
  metadata: {
    'title': 'Black Friday Sale',
    'discount': '50%',
  },
  options: [
    // Popup option
    CampaignPresentumOption(
      surface: AppSurface.popup,
      variant: CampaignVariant.fullscreenDialog,
      maxImpressions: 1,
      cooldownMinutes: null,
      isDismissible: true,
    ),
    // Banner option
    CampaignPresentumOption(
      surface: AppSurface.homeTopBanner,
      variant: CampaignVariant.banner,
      maxImpressions: null,
      cooldownMinutes: 1440, // 24 hours
      isDismissible: true,
      alwaysOnIfEligible: true,
    ),
  ],
)
This lets the same campaign appear as both a popup and a banner, with different rules for each.

Items

Items combine a payload with a specific option:
class CampaignPresentumItem
    extends PresentumItem<CampaignPayload, AppSurface, CampaignVariant> {
  const CampaignPresentumItem({
    required this.payload,
    required this.option,
  });

  @override
  final CampaignPayload payload;

  @override
  final CampaignPresentumOption option;
}
Items provide derived properties:
final item = CampaignPresentumItem(
  payload: campaign,
  option: campaign.options.first,
);

item.id          // "black-friday-2025::fullscreenDialog::popup"
item.surface     // AppSurface.popup
item.variant     // CampaignVariant.fullscreenDialog
item.priority    // 100 (from payload)
item.metadata    // {'title': 'Black Friday Sale', ...}
item.stage       // 0 (from option)

Creating items

Convert payloads to items when feeding to engine:
final payload = CampaignPayload(/* ... */);

// Create items from all options
final items = payload.options.map((option) {
  return CampaignPresentumItem(
    payload: payload,
    option: option,
  );
}).toList();

// Feed to engine
engine.setCandidatesWithDiff((state) => items);

Type aliases

Use type aliases for cleaner code:
// Define once
typedef CampaignItem = CampaignPresentumItem;
typedef CampaignGuard = PresentumGuard<CampaignItem, CampaignSurface, CampaignVariant>;
typedef CampaignState = PresentumState<CampaignItem, CampaignSurface, CampaignVariant>;

// Use everywhere
class MyGuard extends CampaignGuard {
  @override
  FutureOr<CampaignState> call(...) async {
    // Much cleaner!
  }
}
See production typedefs ->

Metadata patterns

Campaign metadata

metadata: {
  'title': 'Black Friday Sale',
  'message': '50% off premium features',
  'imageUrl': 'https://...',
  'ctaText': 'Shop Now',
  'ctaUrl': '/shop/black-friday',
  'backgroundColor': '#1E293B',
  'time_range': {
    'start': '2025-11-29T00:00:00Z',
    'end': '2025-12-02T23:59:59Z',
  },
  'required_segments': ['active_users'],
  'is_active': true,
}

App update metadata

metadata: {
  'version': '2.0.0',
  'buildNumber': 42,
  'isForced': false,
  'releaseNotes': 'New features and improvements',
  'downloadUrl': 'https://...',
  'minRequiredVersion': '1.5.0',
}

Tip metadata

metadata: {
  'title': 'Swipe to refresh',
  'description': 'Pull down to see latest updates',
  'iconName': 'refresh',
  'helpUrl': '/help/refresh',
  'requiredCompletedSteps': ['signup', 'onboarding'],
}

Best practices

Flat metadata is easier to work with in eligibility extractors:Good:
metadata: {
  'title': 'Sale',
  'discount': '50%',
  'start_date': '2025-11-29',
}
Acceptable:
metadata: {
  'title': 'Sale',
  'time_range': {
    'start': '2025-11-29',
    'end': '2025-12-02',
  },
}
Establish priority ranges for different types:
  • 0-99: Low priority tips
  • 100-199: Regular campaigns
  • 200-299: Important updates
  • 300+: Critical alerts (maintenance, force updates)
Add fromJson/toJson for Remote Config integration:
factory CampaignPayload.fromJson(Map<String, Object?> json) {
  // Deserialize
}

Map<String, Object?> toJson() {
  // Serialize
}

Next steps