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.
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 pull eligibility conditions from your payloads:
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),
),
];
}
}
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