Skip to main content

Overview

The eligibility system helps you declaratively check if presentations should be shown based on complex conditions like time ranges, user segments, app versions, feature flags, and more. Instead of writing nested if/else statements, you compose Eligibility conditions that are evaluated by EligibilityRules.
The eligibility system is optional but recommended for complex targeting logic. You can still use manual checks in guards if you prefer.

Dynamic eligibility from JSON

Most commonly, eligibility conditions come from payload metadata stored as JSON and can be updated in real-time without releasing a new app version. Typical workflow:
  1. Store eligibility conditions in Firebase Remote Config or your backend API
  2. Fetch JSON payloads with embedded eligibility metadata
  3. Extractors pull conditions from metadata
  4. Resolver evaluates conditions against runtime context
  5. Update eligibility rules remotely without app updates

Basic condition JSON

Simple eligibility condition in payload metadata:
{
  "id": "black-friday-2025",
  "priority": 100,
  "metadata": {
    "title": "Black Friday Sale",
    "discount": "50%",
    "time_range": {
      "start": "2025-11-29T00:00:00Z",
      "end": "2025-12-02T23:59:59Z"
    },
    "is_active": true
  },
  "options": [...]
}

Nested condition JSON

Complex eligibility with multiple conditions:
{
  "id": "premium-upsell-2025",
  "priority": 80,
  "metadata": {
    "title": "Upgrade to Premium",
    "eligibility": {
      "all_of": [
        {
          "time_range": {
            "start": "2025-01-01T00:00:00Z",
            "end": "2025-12-31T23:59:59Z"
          }
        },
        {
          "any_of": [
            {
              "user_segments": ["trial", "free_tier"]
            },
            {
              "numeric_comparison": {
                "key": "days_since_signup",
                "operator": "greater_than",
                "value": 7
              }
            }
          ]
        },
        {
          "not": {
            "boolean_flag": {
              "key": "has_premium",
              "value": true
            }
          }
        }
      ]
    }
  },
  "options": [...]
}
When eligibility is in Firebase Remote Config, you can update targeting rules, time ranges, and conditions in real-time across all users without deploying a new app version.
See Firebase Remote Config integration →

Quick example

// Define eligibility conditions
final eligibility = AllOfEligibility(conditions: [
  TimeRangeEligibility(
    start: DateTime(2025, 12, 1),
    end: DateTime(2025, 12, 31),
  ),
  AnySegmentEligibility(
    contextKey: 'user_segments',
    requiredSegments: {'premium', 'verified'},
  ),
  NumericComparisonEligibility(
    contextKey: 'app_version',
    comparison: NumericComparison.greaterThanOrEqual,
    threshold: 2.0,
  ),
]);

// Create resolver
final resolver = DefaultEligibilityResolver(
  rules: createStandardRules(),
  extractors: [
    TimeRangeExtractor(),
    AnySegmentExtractor(),
    NumericComparisonExtractor(),
  ],
);

// Evaluate
final context = {
  'user_segments': {'premium', 'trial'},
  'app_version': 2.1,
};

final isEligible = await resolver.isEligible(campaign, context);

Built-in conditions

TimeRangeEligibility

Show only during specific time periods:
TimeRangeEligibility(
  start: DateTime(2025, 11, 29),  // Black Friday starts
  end: DateTime(2025, 12, 2),     // Cyber Monday ends
)

AnySegmentEligibility

User must belong to at least one segment:
AnySegmentEligibility(
  contextKey: 'user_segments',
  requiredSegments: {'premium', 'verified', 'early_adopter'},
)
Context must provide:
{
  'user_segments': {'premium', 'active'},  // Set or List
}

SetMembershipEligibility

Value must be in allowed set:
SetMembershipEligibility(
  contextKey: 'user_country',
  allowedValues: {'US', 'CA', 'UK'},
)

BooleanFlagEligibility

Boolean flag must match:
BooleanFlagEligibility(
  contextKey: 'is_premium',
  requiredValue: true,
)

NumericComparisonEligibility

Numeric comparisons:
NumericComparisonEligibility(
  contextKey: 'app_version',
  comparison: NumericComparison.greaterThanOrEqual,
  threshold: 2.0,
)
Comparison operators:
  • lessThan - Less than
  • lessThanOrEqual - Less than or equal
  • equal - Equal
  • greaterThanOrEqual - Greater than or equal
  • greaterThan - Greater than
  • notEqual - Not equal

StringMatchEligibility

Regex pattern matching:
StringMatchEligibility(
  contextKey: 'device_model',
  pattern: r'^iPhone\d+',
  caseSensitive: false,
)

Combining conditions

AllOfEligibility (AND)

ALL conditions must pass:
AllOfEligibility(conditions: [
  TimeRangeEligibility(/* ... */),
  AnySegmentEligibility(/* ... */),
  BooleanFlagEligibility(/* ... */),
])

AnyOfEligibility (OR)

At least ONE condition must pass:
AnyOfEligibility(conditions: [
  SetMembershipEligibility(
    contextKey: 'user_country',
    allowedValues: {'US'},
  ),
  SetMembershipEligibility(
    contextKey: 'user_country',
    allowedValues: {'CA'},
  ),
])

NotEligibility (NOT)

Invert condition:
NotEligibility(
  condition: BooleanFlagEligibility(
    contextKey: 'has_purchased',
    requiredValue: true,
  ),
)
// Shows only to users who haven't purchased

Complex example

Combine conditions for sophisticated targeting:
final eligibility = AllOfEligibility(conditions: [
  // Must be during Black Friday weekend
  TimeRangeEligibility(
    start: DateTime(2025, 11, 29),
    end: DateTime(2025, 12, 2),
  ),

  // AND user is premium or verified
  AnyOfEligibility(conditions: [
    AnySegmentEligibility(
      contextKey: 'user_segments',
      requiredSegments: {'premium'},
    ),
    AnySegmentEligibility(
      contextKey: 'user_segments',
      requiredSegments: {'verified'},
    ),
  ]),

  // AND app version >= 2.0
  NumericComparisonEligibility(
    contextKey: 'app_version',
    comparison: NumericComparison.greaterThanOrEqual,
    threshold: 2.0,
  ),

  // AND NOT already purchased
  NotEligibility(
    condition: BooleanFlagEligibility(
      contextKey: 'has_purchased_premium',
      requiredValue: true,
    ),
  ),
]);

Extractors

Extractors pull eligibility conditions from your payload metadata JSON. This is how you convert JSON from Firebase Remote Config or APIs into typed Eligibility objects.

Basic extractor

Extract simple conditions from metadata:
class TimeRangeExtractor
    implements EligibilityExtractor<CampaignPayload> {
  const TimeRangeExtractor({this.metadataKey = 'time_range'});

  final String metadataKey;

  @override
  List<Eligibility> extract(CampaignPayload subject) {
    final timeRange = subject.metadata[metadataKey] as Map<String, Object?>?;
    if (timeRange == null) return [];

    final startStr = timeRange['start'] as String?;
    final endStr = timeRange['end'] as String?;

    if (startStr == null || endStr == null) return [];

    return [
      TimeRangeEligibility(
        start: DateTime.parse(startStr),
        end: DateTime.parse(endStr),
      ),
    ];
  }
}
Extracts from JSON:
{
  "metadata": {
    "time_range": {
      "start": "2025-11-29T00:00:00Z",
      "end": "2025-12-02T23:59:59Z"
    }
  }
}

Nested extractor

Extract complex nested conditions:
class AnyOfExtractor implements EligibilityExtractor<HasMetadata> {
  const AnyOfExtractor({
    required this.nestedExtractors,
    this.metadataKey = 'any_of',
  });

  final List<EligibilityExtractor<HasMetadata>> nestedExtractors;
  final String metadataKey;

  @override
  List<Eligibility> extract(HasMetadata subject) {
    final anyOfList = subject.metadata[metadataKey] as List<Object?>?;
    if (anyOfList == null) return [];

    final conditions = <Eligibility>[];

    for (final item in anyOfList) {
      final itemMetadata = item as Map<String, Object?>?;
      if (itemMetadata == null) continue;

      // Create temporary payload with nested metadata
      final nestedSubject = _NestedMetadata(itemMetadata);

      // Extract conditions from nested metadata using all extractors
      for (final extractor in nestedExtractors) {
        conditions.addAll(extractor.extract(nestedSubject));
      }
    }

    return conditions.isEmpty ? [] : [AnyOfEligibility(conditions: conditions)];
  }
}
Extracts from JSON:
{
  "metadata": {
    "any_of": [
      {
        "user_segments": ["premium", "verified"]
      },
      {
        "time_range": {
          "start": "2025-12-01T00:00:00Z",
          "end": "2025-12-31T23:59:59Z"
        }
      }
    ]
  }
}
Extractors enable dynamic eligibility updates. Change the JSON in Firebase Remote Config, and all clients automatically get new targeting rules without an app update.

Using in guards

Integrate eligibility checks in your guards:
class CampaignGuard extends PresentumGuard<CampaignItem, AppSurface, CampaignVariant> {
  CampaignGuard(this.eligibilityResolver);

  final EligibilityResolver<CampaignPayload> eligibilityResolver;

  @override
  FutureOr<PresentumState> call(
    storage, history, state, candidates, context,
  ) async {
    for (final candidate in candidates) {
      // Check eligibility
      final isEligible = await eligibilityResolver.isEligible(
        candidate.payload,
        context,
      );

      if (!isEligible) continue;

      state.setActive(candidate.surface, candidate);
    }

    return state;
  }
}

Get ineligible condition

Find out WHY something is ineligible:
final ineligibleCondition = await resolver.getIneligibleCondition(
  campaign,
  context,
);

if (ineligibleCondition case TimeRangeEligibility(:final start)) {
  // Campaign hasn't started yet - schedule for later
  scheduleForFuture(campaign, start);
} else if (ineligibleCondition != null) {
  // Campaign is ineligible for other reasons - remove it
  removeCampaign(campaign);
}
See production usage ->

Production example

Here’s how a production app uses eligibility:
// Define resolver
final eligibilityResolver = const DefaultEligibilityResolver<CampaignPayload>(
  rules: [
    TimeRangeRule(),
    AnySegmentRule(),
    ConstantRule(),
  ],
  extractors: [
    TimeRangeExtractor(),
    AnySegmentExtractor(),
    ConstantExtractor(metadataKey: 'is_active'),
  ],
);

// Use in guards
class SchedulingGuard extends CampaignGuard {
  SchedulingGuard(this.eligibility);

  final EligibilityResolver<CampaignPayload> eligibility;

  @override
  FutureOr<CampaignPresentumState> call(
    storage, history, state, candidates, context,
  ) async {
    // Filter eligible campaigns
    final eligibleIds = <String>{};

    for (final campaign in candidates.map((e) => e.payload).toSet()) {
      final isEligible = await eligibility.isEligible(campaign, context);
      if (isEligible) eligibleIds.add(campaign.id);
    }

    // Process only eligible campaigns
    final eligible = candidates
        .where((c) => eligibleIds.contains(c.payload.id))
        .toList();

    // Apply scheduling logic...

    return state;
  }
}
Full scheduling guard ->

Create custom conditions

Define your own eligibility conditions:
// 1. Create condition
@immutable
class GeoLocationEligibility extends Eligibility {
  const GeoLocationEligibility({
    required this.allowedCountries,
    required this.allowedRegions,
  });

  final Set<String> allowedCountries;
  final Set<String>? allowedRegions;
}

// 2. Create rule
class GeoLocationRule implements EligibilityRule<GeoLocationEligibility> {
  const GeoLocationRule();

  @override
  bool supports(Eligibility eligibility) =>
      eligibility is GeoLocationEligibility;

  @override
  Future<bool> evaluate(
    GeoLocationEligibility eligibility,
    Map<String, dynamic> context,
  ) async {
    final country = context['user_country'] as String?;
    final region = context['user_region'] as String?;

    if (country == null) return false;

    final countryMatch = eligibility.allowedCountries.contains(country);
    if (!countryMatch) return false;

    if (eligibility.allowedRegions case final regions?) {
      return region != null && regions.contains(region);
    }

    return true;
  }
}

// 3. Create extractor
class GeoLocationExtractor implements EligibilityExtractor<CampaignPayload> {
  const GeoLocationExtractor();

  @override
  List<Eligibility> extract(CampaignPayload subject) {
    final geo = subject.metadata['geo_targeting'] as Map<String, Object?>?;
    if (geo == null) return [];

    final countries = (geo['countries'] as List?)?.cast<String>().toSet();
    final regions = (geo['regions'] as List?)?.cast<String>().toSet();

    if (countries == null) return [];

    return [
      GeoLocationEligibility(
        allowedCountries: countries,
        allowedRegions: regions,
      ),
    ];
  }
}

// 4. Use in resolver
final resolver = DefaultEligibilityResolver(
  rules: [
    ...createStandardRules(),
    const GeoLocationRule(),
  ],
  extractors: [
    const TimeRangeExtractor(),
    const AnySegmentExtractor(),
    const GeoLocationExtractor(),
  ],
);

Next steps