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

# Eligibility system

> Build complex eligibility rules with conditions, rules, and extractors

## 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 `EligibilityRule`s.

<Tip>
  The eligibility system is optional but recommended for complex targeting
  logic. You can still use manual checks in guards if you prefer.
</Tip>

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

```json theme={null}
{
  "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:

```json theme={null}
{
  "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": [...]
}
```

<Info>
  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.
</Info>

[See Firebase Remote Config integration →](/recipes/remote-config)

## Quick example

```dart theme={null}
// 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:

```dart theme={null}
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:

```dart theme={null}
AnySegmentEligibility(
  contextKey: 'user_segments',
  requiredSegments: {'premium', 'verified', 'early_adopter'},
)
```

Context must provide:

```dart theme={null}
{
  'user_segments': {'premium', 'active'},  // Set or List
}
```

### SetMembershipEligibility

Value must be in allowed set:

```dart theme={null}
SetMembershipEligibility(
  contextKey: 'user_country',
  allowedValues: {'US', 'CA', 'UK'},
)
```

### BooleanFlagEligibility

Boolean flag must match:

```dart theme={null}
BooleanFlagEligibility(
  contextKey: 'is_premium',
  requiredValue: true,
)
```

### NumericComparisonEligibility

Numeric comparisons:

```dart theme={null}
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:

```dart theme={null}
StringMatchEligibility(
  contextKey: 'device_model',
  pattern: r'^iPhone\d+',
  caseSensitive: false,
)
```

## Combining conditions

### AllOfEligibility (AND)

ALL conditions must pass:

```dart theme={null}
AllOfEligibility(conditions: [
  TimeRangeEligibility(/* ... */),
  AnySegmentEligibility(/* ... */),
  BooleanFlagEligibility(/* ... */),
])
```

### AnyOfEligibility (OR)

At least ONE condition must pass:

```dart theme={null}
AnyOfEligibility(conditions: [
  SetMembershipEligibility(
    contextKey: 'user_country',
    allowedValues: {'US'},
  ),
  SetMembershipEligibility(
    contextKey: 'user_country',
    allowedValues: {'CA'},
  ),
])
```

### NotEligibility (NOT)

Invert condition:

```dart theme={null}
NotEligibility(
  condition: BooleanFlagEligibility(
    contextKey: 'has_purchased',
    requiredValue: true,
  ),
)
// Shows only to users who haven't purchased
```

## Complex example

Combine conditions for sophisticated targeting:

```dart theme={null}
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:

```dart theme={null}
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:**

```json theme={null}
{
  "metadata": {
    "time_range": {
      "start": "2025-11-29T00:00:00Z",
      "end": "2025-12-02T23:59:59Z"
    }
  }
}
```

### Nested extractor

Extract complex nested conditions:

```dart theme={null}
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:**

```json theme={null}
{
  "metadata": {
    "any_of": [
      {
        "user_segments": ["premium", "verified"]
      },
      {
        "time_range": {
          "start": "2025-12-01T00:00:00Z",
          "end": "2025-12-31T23:59:59Z"
        }
      }
    ]
  }
}
```

<Tip>
  Extractors enable dynamic eligibility updates. Change the JSON in Firebase
  Remote Config, and all clients automatically get new targeting rules without
  an app update.
</Tip>

## Using in guards

Integrate eligibility checks in your guards:

```dart theme={null}
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:

```dart theme={null}
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 ->](https://github.com/itsezlife/presentum/blob/748390c7f925a6121dd46447fbfe823dd0bc8a97/example/lib/src/campaigns/presentum/campaigns_provider.dart#L265)

## Production example

Here's how a production app uses eligibility:

```dart theme={null}
// 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 ->](https://github.com/itsezlife/presentum/blob/master/example/lib/src/campaigns/presentum/guards/scheduling_guard.dart)

## Create custom conditions

Define your own eligibility conditions:

```dart theme={null}
// 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

<CardGroup cols={2}>
  <Card title="Guards" icon="shield" href="/core-concepts/guards">
    Use eligibility in guards
  </Card>

  {" "}

  <Card title="Production example" icon="code" href="https://github.com/itsezlife/presentum/blob/master/example/lib/src/campaigns/presentum/campaigns_presentum_state_mixin.dart">
    See real-world usage
  </Card>
</CardGroup>
