Skip to main content

Customizing Tenure Badges

Three filters give developers full control over how tenure badges are computed and rendered. They compose cleanly — each one fires at a different point in the pipeline, so you can use any combination without interference.

Filter Fires from What you control
benecaster_tenure_badge_milestones TenureBadgeCalculator::resolve_milestones() The threshold map — which month counts produce which labels
benecaster_tenure_badge_label TenureBadgeCalculator::build_snapshot_from_row() The selected label, per subscriber, after the map is matched
benecaster_tenure_badge_output BadgeChipRenderer::render_chip() The chip HTML for tenure_auto rows

Two Computed Rows on benecaster_user_badges

The tenure chip described below isn’t the only row Benecaster appends to the badge list. Benecaster registers two callbacks on the benecaster_user_badges filter:

Callback Priority source Fires when
FollowerBadgeAppender::append_follower_badge 15 follower_auto A show is in context and the user’s token on it is a follower’s
TenureBadgeAppender::append_tenure_badge 20 tenure_auto The tenure toggle is on and a snapshot resolves (see Subscriber Tenure Badges)

Both are computed, synthesized rows — neither is a persisted badge grant, and neither can be manually assigned or removed from the admin UI. Register your own callback at a priority later than 15 if you want to see, or filter out, the Follower chip — anything registered at 15 or earlier runs before it exists in the array.

When a show is in context, the outer .benecaster-user-badges wrapper carries an is-follower or is-subscriber class plus a data-token-type attribute, and any chip whose row knows its token type — the Follower chip and the tenure chip both do — carries the same class. With no show in context (the account page’s site-wide render), none of this classification is added, because one person can follow one show while paying for another.


Redefine the Threshold Map

Filter: benecaster_tenure_badge_milestones

Replace the entire milestone map with your own month → label pairs. The calculator picks the highest threshold that does not exceed the subscriber’s tenure and uses that entry’s label.

add_filter( 'benecaster_tenure_badge_milestones', function ( array $default ): array {
    return [
        3  => 'First-Quarter Supporter',
        6  => 'Half-Year Supporter',
        12 => 'One-Year Anniversary',
        24 => 'Two-Year Anniversary',
    ];
} );

Normalization: the calculator sorts your map ascending and drops entries with non-positive month keys or non-string / empty labels. Returning a non-array falls back to the built-in defaults. Return order does not matter.

Below-first-milestone subscribers: a subscriber whose tenure is below your lowest threshold receives an empty label — the chip is suppressed unless benecaster_tenure_badge_label returns a non-empty string for them.


Override the Matched Label

Filter: benecaster_tenure_badge_label

Fires after the milestone map is matched. Use this when the label needs to vary per subscriber in ways the threshold map alone cannot express: localisation, a special designation for very long-tenure members, or injecting a “New Supporter” label below the first milestone.

add_filter(
    'benecaster_tenure_badge_label',
    function ( string $label, int $user_id, int $show_id, int $months_since_join ): string {
        // Charter Member designation for anyone past three years.
        if ( $months_since_join >= 36 ) {
            return __( 'Charter Member', 'my-addon' );
        }
        return $label;
    },
    10, 4
);

Inject a label for below-first-milestone subscribers:

add_filter(
    'benecaster_tenure_badge_label',
    function ( string $label, int $user_id, int $show_id, int $months_since_join ): string {
        // $label is '' when tenure is below the lowest milestone threshold.
        // Return a non-empty string to show a chip for new members.
        return $label !== '' ? $label : __( 'New Supporter', 'my-addon' );
    },
    10, 4
);

$show_id notes:

  • On per-show surfaces (Supporter Wall), $show_id is the show being rendered.
  • On the site-wide account page, the calculator uses the earliest active token across all shows and passes that show’s ID as $show_id.
  • $show_id === 0 only when the user has no active tokens at all.

Replace the Chip HTML

Filter: benecaster_tenure_badge_output

Fires from BadgeChipRenderer::render_chip() only for tenure_auto badge rows. Tier and manual badges are unaffected. Use this to wrap the chip, add a tooltip, link it to a page, or replace it with entirely custom markup.

add_filter(
    'benecaster_tenure_badge_output',
    function ( string $html, int $user_id, int $show_id ): string {
        $tip = esc_attr__( 'Based on your subscription start date', 'my-addon' );
        return '<span class="my-tenure-tooltip" data-tip="' . $tip . '">' . $html . '</span>';
    },
    10, 3
);

$show_id is 0 when the chip renders on the site-wide account page (per-user mode).


Composing All Three Together

The filters fire in order: milestones → label → output, each handing its result to the next. Running all three gives you complete control over the badge, from which tenures earn one through to the markup it renders as.

That full pipeline is a recipe rather than a snippet, because it is something you copy and adapt whole:

→ Customize tenure badges end to end — a quarterly milestone map, a “Charter Member” designation past three years, and a chip that links to an explainer page. Use whichever stages you need and drop the rest.


The Same Pattern: the Explicit-Content Badge

Tenure badges are not the only first-party badge Benecaster renders, and the second one follows the same shape — so what you learn here transfers.

The explicit-content badge marks episodes as explicit on the episode page, archive cards, [benecaster_episodes] lists, and the [benecaster_player] title strip. Like the tenure chip, it is customised CSS class first, filter second:

Want to Reach for
Restyle it CSS alone — the class list already carries the modifiers
Change the wording benecaster_explicit_badge_label
Change which episodes get one benecaster_explicit_badge_mode
Replace the markup benecaster_explicit_badge_html

The filters fire in the same cascade the tenure badge uses — mode → label → html, each handing its result to the next.

One thing differs, and it is worth knowing. The explicit badge carries a second class modifier naming the surface it is rendering on:

benecaster-explicit-badge--explicit benecaster-explicit-badge--context-card

--{mode} is clean, yes, or explicit. --context-{surface} is single, card, list, player, or shortcode. Because the same badge appears in five places with quite different space budgets, that second modifier lets one stylesheet show full text on the episode page and a compact marker in an archive grid — no filter, no template override.

→ Restyle or reword the Explicit badge — the three common asks, one filter each.


Checking Whether the Feature Is Active

Add-ons and theme templates can check the site-wide toggle without reading wp_options directly:

if ( \Benecaster\Membership\TenureBadgeAppender::is_enabled() ) {
    // Tenure badge is on — safe to render tenure-specific UI.
}

See TenureBadgeAppender and TenureBadgeCalculator for the full class references.

See Also

Need this built rather than just documented? See our services →