Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,23 @@ Dismissal is keyed by ID, not version: a user who dismissed 1.0.0 does not see 1

1. The request is not for wp-admin: `is_admin()` is false.
2. Another copy of the package already rendered the same banner ID on this request.
3. The current time is before `start` or at or after `end`.
3. The current time is before `start` or at or after `end`, unless the schedule filter says otherwise.
4. The current user dismissed it.
5. The callable was passed and returned false.

The callable runs last and only when it can still matter, so an expensive check costs nothing on a
banner that would not show anyway.

Filter `nx_banners_scheduled:{id}` to decide step 3 yourself, such as to see a banner in QA before it
starts, without moving the site's clock:

```php
add_filter( 'nx_banners_scheduled:nexcess-black-friday-2026', '__return_true' );
```

It receives whether the banner is inside its schedule. Return false to hide a banner inside it. The
filter applies only when the plugin that registered the banner bundles version 1.1.0 or later.

When two plugins render the same banner ID on one request, the first to call `render()` prints it and
the other prints nothing. Different IDs each render.

Expand Down Expand Up @@ -238,6 +248,7 @@ Everything shared between plugins is therefore reached through a string key:
| `nx-banners-dismiss` | Dismiss query argument carrying the banner ID. The nonce action is `nx-banners-dismiss:{id}`. |
| `nx-banners-nonce` | Nonce query argument, named so it cannot replace a `_wpnonce` already in the page URL. |
| `nx_banners_rendered:{id}` | Action fired when a banner renders; later copies check `did_action()` and stand down. |
| `nx_banners_scheduled:{id}` | Filter: whether the banner is inside its schedule, as a bool. |
| `nx-banners-{id}` | Handle of the banner's stylesheet in WordPress's style registry. |

A registered banner is called only through its methods, never checked by class: `version()` while
Expand Down
11 changes: 10 additions & 1 deletion src/Banner.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ final class Banner {
*/
private const RENDERED_ACTION = 'nx_banners_rendered:';

/**
* Prefix of the filter on whether a banner is in its schedule. Shared across copies, so it applies
* whichever copy renders the ID.
*/
private const SCHEDULED_FILTER = 'nx_banners_scheduled:';

/**
* Prefix of the handle a banner's stylesheet registers under.
*/
Expand Down Expand Up @@ -140,8 +146,11 @@ private function printTemplate( ?string $template, BannerFields $fields, string
private function isScheduled(): bool {
$now = time();

return ( $this->start === null || $now >= $this->start->getTimestamp() )
$scheduled = ( $this->start === null || $now >= $this->start->getTimestamp() )
&& ( $this->end === null || $now < $this->end->getTimestamp() );

// Lets QA show a banner outside its schedule, or hide one inside it, without moving the clock.
return (bool) apply_filters( self::SCHEDULED_FILTER . $this->id, $scheduled );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ final class Package {
/**
* Bump with every release: the bundled stylesheet's URL carries it, so browsers fetch the new file.
*/
public const VERSION = '1.0.0';
public const VERSION = '1.1.0';

}
29 changes: 29 additions & 0 deletions tests/BannerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Nexcess\Banners\Tests;

use Brain\Monkey\Actions;
use Brain\Monkey\Filters;
use Brain\Monkey\Functions;
use DateTimeImmutable;
use DateTimeZone;
Expand Down Expand Up @@ -258,6 +259,34 @@ public function testTheScheduleComparesMomentsNotWallClocksAcrossTimeZones(): vo
$this->assertNotSame( '', $this->render( $banner ) );
}

/**
* QA can see a banner before it starts without moving the site's clock.
*/
public function testTheScheduleFilterCanShowABannerBeforeItsStart(): void {
$this->stubDismissed( [] );
$this->stubRenderable();
Filters\expectApplied( 'nx_banners_scheduled:promo' )->once()->with( false )->andReturn( true );

$this->assertNotSame( '', $this->render( new Banner( 'promo', '1.0.0', new BannerFields(), self::PACKAGE_URL, new DateTimeImmutable( '+1 hour' ) ) ) );
}

public function testTheScheduleFilterCanHideABannerInsideItsSchedule(): void {
$this->stubDismissed( [] );
Filters\expectApplied( 'nx_banners_scheduled:promo' )->once()->with( true )->andReturn( false );

$this->assertSame( '', $this->render( new Banner( 'promo', '1.0.0', new BannerFields(), self::PACKAGE_URL, new DateTimeImmutable( '-1 hour' ) ) ) );
}

/**
* The filter is per banner ID, so faking one banner's schedule never moves another's.
*/
public function testTheScheduleFilterOfAnotherBannerChangesNothing(): void {
$this->stubDismissed( [] );
Filters\expectApplied( 'nx_banners_scheduled:other' )->never();

$this->assertSame( '', $this->render( new Banner( 'promo', '1.0.0', new BannerFields(), self::PACKAGE_URL, new DateTimeImmutable( '+1 hour' ) ) ) );
}

/**
* Every plugin bundles its own prefixed copy, so the second Banner here stands in for another
* plugin rendering the same campaign later on the same request.
Expand Down
Loading