GraceExpiryTracker
\Benecaster\License\GraceExpiryTracker
Owns the plugin-side payment-failure grace window: when it opened, whether the Day-30 hard cutoff has been applied, and the token bookkeeping at both the cutoff and the recovery. Consumed by FeedController both to keep serving private feeds while the license is Suspended but still inside the window, and to force 403 once the cutoff is in force.
Emails are not this class’s job. Podcaster-facing payment-failure notices are sent by the license server against the account holder’s billing address — one message per license per event rather than one per install. The plugin’s responsibility is limited to local token and feed behavior, plus the observability actions below.
What subscribers see at cutoff. A hard 403 (“feeds go dark”). There is no redirect to the show’s public feed, and no filter and no setting that changes it.
Why there is no redirect. A lapsed podcaster’s paying subscribers were silently moved onto public episodes with no error and no signal to anyone. The subscriber believed they were still receiving what they paid for, and the podcaster lost the one symptom that would have prompted them to fix their billing. benecaster_grace_period_feed_fallback is retired rather than merely re-defaulted: its own documented one-liner redirected the show’s public feed to itself, an infinite loop cached for an hour, and a filter whose only purpose is to defeat the go-dark policy is the “third tier of degraded premium” the floor rules out.
The 403 has a dedicated branch — FeedController::feed_is_dark(). Two paths reach the same 403, and knowing which is which matters because only one of them involves revocation:
• Payment hard cutoff revokes every active token, so a later poll is simply an invalid-token request and lands on the branch that already exists for those.
• feed_is_dark() 403s every token-bearing request before token validation, which is how the cancelled and expired license states reach the same ending — nothing revokes their tokens, so those tokens are still perfectly valid and an invalid-token branch would never see them. This is also why the gate runs on token-bearing requests only: an anonymous public-feed request must pass straight through.
The per-show “serve public feed on invalid token” toggle is bypassed on both paths. It governs subscriber-level failures — an invalid token, a lapsed membership — and this is a podcaster-level one.
Registered automatically on plugin boot — do not instantiate directly.
Constructor Dependencies
| Type | Description |
|---|---|
\Benecaster\License\OverLimitFlagManager |
Retained for backwards compatibility only. No in-repo caller uses it since the Day-30 policy changed from flagging tokens over-limit to revoking them outright. |
\Benecaster\Token\TokenRepository |
Performs the bulk revoke at cutoff and the bulk restore at recovery. |
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
is_grace_active(): bool |
Public | — | True only while a grace window is open AND fewer than 30 days have elapsed. FeedController uses this to keep serving private feeds despite the license server reporting Suspended. |
is_hard_cutoff(): bool |
Public | — | True once the Day-30 cutoff has been applied and not yet cleared by recovery. FeedController checks this before honouring a show's serve_public_feed_on_invalid_token setting — while true, revoked tokens get 403 unconditionally and the per-show fallback is bypassed. |
get_elapsed_days(): int |
Public | — | Whole days since the grace window opened, or 0 when none is active. Useful for escalating an admin notice as the deadline approaches. |
get_started_at(): int |
Public | — | Unix timestamp when the current grace window opened, or 0 if none is active. |
maybe_flag_grace_expired(): void |
Public | — | Daily cron callback. On the first tick that crosses day 30, bulk-revokes every active token via TokenRepository::revoke_all_active_with_reason('grace_timeout'), stamps OPTION_HARD_CUTOFF_AT, fires benecaster_clear_feed_cache once, and fires benecaster_license_payment_grace_hard_cutoff. Idempotent — later ticks find the cutoff option already set and do nothing. |
on_license_validated( array $response ): void |
Public | — | Recovery path, called from LicenseValidationCron when the license server returns valid. Clears the grace window and fires benecaster_license_grace_recovered. If a hard cutoff was in force, additionally restores every token tagged 'grace_timeout' via TokenRepository::restore_revoked_with_reason(), clears OPTION_HARD_CUTOFF_AT, and fires benecaster_license_payment_grace_recovered with the restored count. Idempotent — a second recovery signal early-returns because the grace timestamp is already cleared, so nothing is restored twice and no action re-fires. |
Constants
| Name | Value | Description |
|---|---|---|
OPTION_GRACE_STARTED_AT |
'benecaster_license_grace_started_at' |
wp_options key holding the Unix timestamp when the grace window opened. Cleared on recovery. |
OPTION_HARD_CUTOFF_AT |
'benecaster_license_payment_grace_hard_cutoff_at' |
wp_options key holding the Unix timestamp when the Day-30 cutoff was applied. Presence of a non-zero value is what is_hard_cutoff() reports. Cleared on recovery. |
REVOCATION_REASON_GRACE_TIMEOUT |
'grace_timeout' |
Revocation reason recorded against every token revoked at cutoff. Recovery restores only tokens carrying this reason — via TokenRepository::restore_revoked_with_reason() — so admin, bridge, and manual-grant revocations made during the same window are left alone. |
GRACE_DAYS |
30 |
Length of the grace window in days. |
Hooks Fired
Notes
There are no OPTION_PAYMENT_NOTIFIED_AT or OPTION_LAST_REMINDER_DAY constants. Podcaster-facing
payment emails are sent by the license server, so the plugin keeps no reminder state. Code reading either
constant will fatal.
Constructor: TokenRepository is a required second parameter. OverLimitFlagManager
is in first position for backwards compatibility but is unused.