benecaster_promote_grace_expiry_should_revoke
Decides whether a transferred subscriber’s grace period should actually end. Runs once per past-due row at the top of the daily expiry cron. Promote-to-Bridge checks the destination bridge’s is_user_active( $user_id, $show_id ) first, for the four core bridges, and sets $should_revoke to false when the subscriber is confirmed active there (true otherwise, including when the bridge can’t answer — e.g. a deactivated plugin). Your callback receives that computed default and has the final word: return it unchanged to defer to the built-in check, or override it either direction. Return false to veto.
A veto skips the entire expiry, not just the revoke — the token revocation, the expired_grace status transition, the promote_grace_expired email, and the benecaster_promote_grace_expired action. That is deliberate: skipping only the revoke would email a subscriber that their subscription had ended while their feed still worked.
A veto means “not today”, not “never”. Vetoing does not clear promote_grace_period_ends_at, so the row stays past-due and the next daily tick asks again. That built-in re-ask is why there is no “defer by N days” return value — and why one would not work anyway, since 1 is truthy and would read as consent.
A callback that only ever returns false leaves the row permanently past-due, and Promotion History will show it as in-grace with a date that has already elapsed. The pattern to follow is veto today, then resolve the row properly — PromotionService::extend_grace_period() to push the deadline out, or clear_grace_period() when the subscriber has re-authorized.
The return is compared with a strict true !==, so a truthy non-boolean vetoes rather than consenting. Return a real boolean.
Automatic re-authorization detection, described on Promote to Bridge — Overview, is built on this same filter for the four core bridges. Use it yourself for what that built-in check doesn’t cover: a custom bridge, a check more specific than plain active/inactive, or resolving a row the built-in check merely vetoed (it never calls clear_grace_period()).
Override the built-in re-authorization check, or resolve a row it merely vetoed
By default, Promote-to-Bridge already checks this for you, for the four core bridges: every daily cron tick asks the destination bridge’s is_user_active( $user_id, $show_id ) before deciding whether to expire a row, and a subscriber confirmed active there is kept automatically, no filter required. Use this recipe when that built-in check isn’t enough:
- A custom bridge. Your own
BridgeInterfaceimplementation’sis_user_active()doesn’t answer the question the way you need. - A more specific check. The example below checks for an active payment method, which plain active/inactive can’t tell you.
- Resolving the row. The built-in check only vetoes for the day — it never calls
clear_grace_period()— so a row it keeps still shows as past-due in Promotion History forever, re-asked fresh on every tick. If you want the row to stop looking stuck, you still have to resolve it yourself.
benecaster_promote_grace_expiry_should_revoke runs in the daily cron for every row whose grace period has elapsed, after the built-in active-in-destination-bridge check has set its default (false if the subscriber is confirmed active there, true otherwise — including when the bridge can’t answer, e.g. a deactivated plugin). Your callback receives that default as $should_revoke and has the final word — return it unchanged to defer to the built-in check, or override it either direction. Returning false skips the entire expiry for that row: the token is not revoked, the status does not move to expired_grace, the “your podcast subscription has ended” email is not queued, and benecaster_promote_grace_expired does not fire.
That last point is why this filter composes cleanly with Push promote-grace expiry events into a CRM — the action never fires for a row you vetoed, so a CRM listener will not record a churn that did not happen.
Notes
A veto means “not today”, not “never”. Vetoing deliberately leaves promote_grace_period_ends_at untouched, so the row is still past-due and tomorrow’s cron tick offers it to your filter again. That is why the filter takes a plain bool and has no “defer by N days” return path — the re-ask is built in.
It also means a callback that only ever returns false leaves the row permanently past-due, and the podcaster will see it sitting in Promotion History as in-grace with an elapsed date. Resolve the row once you know the answer: call clear_grace_period() when the subscriber has genuinely re-authorized, or extend_grace_period() to move the deadline to a date the podcaster can see.
The veto is evaluated with a strict true !== comparison, so returning a truthy non-boolean — a 1, a non-empty string — vetoes rather than consenting. Return real booleans.
$promotion is the hydrated subscription row: id, user_id, show_id, tier_slug, billing_interval, status, promote_grace_period_ends_at, promoted_to_bridge, the stripe_* columns and the pause columns. $user_id duplicates $promotion['user_id']. Return the incoming $should_revoke rather than a hard-coded true whenever you decline to intervene, so you compose with other callbacks instead of overriding them.
<?php
add_filter( 'benecaster_promote_grace_expiry_should_revoke', function ( bool $should_revoke, array $promotion, int $user_id ): bool {
// Only intervene for the destination plugin we actually know how to ask.
if ( 'memberpress' !== ( $promotion['promoted_to_bridge'] ?? '' ) ) {
return $should_revoke;
}
if ( ! my_destination_plugin_has_active_payment_method( $user_id ) ) {
return $should_revoke; // Let the expiry proceed as normal.
}
// They finished setting up. Resolve the row so it stops coming back to us
// and so the podcaster sees it as cleared in Promotion History.
\Benecaster\Plugin::instance()
->make( \Benecaster\Bridge\PromotionService::class )
->clear_grace_period( (int) $promotion['id'] );
return false;
}, 10, 3 );
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
$should_revoke |
bool |
— | Whether the expiry should proceed. Computed from the destination bridge's `is_user_active()` check before your callback runs — `false` if the subscriber is confirmed active there, `true` otherwise. |
$promotion |
array |
— | The hydrated subscription row that is past due. |
$user_id |
int |
— | Subscriber's WordPress user ID. Duplicates `$promotion['user_id']` for convenience. |
Returns:
bool
Need this built rather than just documented? See our services →