We build Rentals & Bookings, a rental and booking plugin for WooCommerce. The free edition, Lite, is on WordPress.org as Sales Igniter Rentals & Bookings Lite. The paid editions are separate plugins that we sell on our own site.
Lite did not get in on the first try. One review round came back with a guideline 5 finding, trialware, and a warning that more of the same would end the review. This post covers what we learned fixing it, with the code before and after, and a checklist to run before you submit.
What guideline 5 says
The Detailed Plugin Guidelines cover it in two short paragraphs. The first sets the rule:
Plugins may not contain functionality that is restricted or locked, only to be made available by payment or upgrade. Functionality may not be disabled after a trial period or quota is met.
The second gives the way out:
We recommend the use of add-on plugins, hosted outside of WordPress.org, in order to exclude the premium code.
Read together, the test is about what is in the zip. If the code for a feature ships inside the free plugin and something paid decides whether it runs, that is trialware. The way you present it (greyed out, hidden, an upgrade badge) does not change the answer.
What we submitted
Our 1.0.8 build had one gate class, Model/Features.php. Every paid feature had a key, and one filter decided whether it was on:
// Lite 1.0.8, Model/Features.php (trimmed)
class Features {
const INVENTORY_WINDOWS = 'inventory_windows';
// ... eleven more keys: pricing rules, serials, calendar types, pickers, layouts ...
public static function enabled( $feature ) {
// The Regular add-on answers true here.
return (bool) apply_filters( 'sibooking_feature_enabled', false, $feature );
}
/** Inventory Report window lengths, in days. */
public static function inventory_windows() {
return self::enabled( self::INVENTORY_WINDOWS ) ? [ 7, 14, 30 ] : [ 7 ];
}
}
The paid plugin, Regular, hooked that filter and returned true. Lite on its own returned false, so its Inventory Report offered 7 days and left 14 and 30 out of the picker.
We thought we had been careful. A second class, Admin/AddonFields.php, took paid fields off the screen. Its doc block said: "Nothing is greyed out, disabled or overlaid." We had also moved some features out in earlier releases: pricing rules and advanced pricing left Lite in 1.0.4, and serial tracking left in 1.0.8. For the rest we had chosen to hide rather than move, and our own notes said the reviewers might raise them again.
They did.
What the review flagged
The reviewers used the inventory report as their example: a report built into the free plugin that limits itself to seven days unless the paid add-on switches on 14 and 30. They made the point that the rule holds even when the locked code is there "just in case the user upgrades". And they said that if the next review found more issues of the same kind, the plugin would not be reviewed again.
That warning was fair, and it made the job bigger: fixing the one example would not be enough. We had to find every place where Lite carried a capability and waited for Regular to switch it on.
Why a hook that only lifts a cap still counts
Our first reaction was: the paid code isn't in Lite, only a filter is. The 7-day report shows why that's wrong. The code that builds a 14-day or 30-day grid was in Lite. Its windowDays() method accepted 14 and 30 as soon as inventory_windows() listed them. All Regular added was the word true.
A free user installs the plugin. Everything needed for a 30-day report is on their server. One add_filter call turns it on, and our own doc comment even showed that call as a testing tip. That fits "restricted or locked, only to be made available by payment or upgrade" exactly. The filter is the lock, and the paid plugin is the key.
The other gates in the same pattern
Once we searched for every Features::enabled() call, the list was longer than the example:
-
Fixed rental types. A constant named
LOCKED_RENTAL_TYPEShelddatefixed,datetimefixedandfixeddate. Without Regular, the Rental Type select dropped those choices and the server refused to save them. -
Date pickers.
availablePickers()returned only the popup picker unlesscalendar_modeswas on. The inline and separate pickers, their scripts and their preview images all shipped in Lite. -
Product page layouts. The same shape:
availableLayouts()returned one layout unlessproduct_layoutswas on. - Early send and early return. Two settings fields and their section, hidden without Regular.
- Overdue reminders. A reminder kind that refused to send unless the send and return feature was on.
None of these looked like trialware in the admin. Every one of them was.
How we split it
The rule we set: Lite has no feature switches, no caps and no hidden add-on code. Every feature Lite shows is complete in Lite. What Regular sells lives in Regular, with its own PHP, fields, scripts and templates.
In 1.0.9 we deleted Model/Features.php and Admin/AddonFields.php, along with the fixed-length classes, the extra pickers' scripts and the overdue email. In their place Lite got registries. Each registry starts with Lite's own complete entries, and another plugin can add more. The inventory report is the clearest case:
// Lite 1.0.18, Ajax/InventoryReport.php (trimmed)
public static function views() {
$views = [
self::VIEW => [ // 'week'
'label' => '7 days', // translated in the real code
'days' => self::DAYS, // 7
'build' => [ new self(), 'getInventoryReportData' ],
],
];
// A plugin adds a view with its own label, days and build callable.
// The view picker shows only when there is more than one view.
$filtered = apply_filters( 'sibooking_inventory_report_views', $views );
// ... each added view is validated ...
}
Lite's report is a 7-day report. It has no window setting and no code for 14 or 30 days. Regular registers two views, and the grid behind them is Regular's own code:
// Regular, Includes/InventoryReportViews.php (trimmed)
const VIEWS = [ self::FORTNIGHT => 14, self::MONTH => 30 ];
public function init() {
add_filter( 'sibooking_inventory_report_views', [ $this, 'views' ] );
}
public function views( $views ) {
foreach ( self::VIEWS as $key => $days ) {
$views[ $key ] = [
'label' => sprintf( _n( '%d day', '%d days', $days, 'sibooking' ), $days ),
'days' => $days,
'build' => function ( array $request ) use ( $days ) {
return $this->build( $request, $days ); // Regular's own grid builder
},
];
}
return $views;
}
The other gates went the same way. Rental types became the sibooking_rental_types registry, and Regular's FixedRentalTypes class adds the three fixed types with their fields and calendar code. Pickers and layouts became sibooking_date_pickers and sibooking_product_layouts. Overdue and Returned became booking states that Regular registers on sibooking_booking_states.
We could have made the 14- and 30-day views free instead, and we talked about it. We kept them paid and moved the code. Either choice passes review. The one that fails sits in between: code in Lite, switched off.
Some features went the other way: guest types and stays by the night became full Lite features in 1.0.9. Regular kept only its extras, such as guest fees.
Saved data still has to work
A store that used Regular and later runs Lite alone must not break. Each registry has a plain fallback: a product saved with a rental type nobody registers is booked as a plain date calendar, or a date and time calendar if the type's name says it uses a time of day. That fallback contains no fixed-length logic, so it is a safety net, not a feature.
The paid plugin bundles its own Lite
Regular ships with a copy of Lite inside it, so a customer installs one plugin. Since the split, the two move in lockstep. A lite.lock file pins the exact tagged Lite commit the release build bundles, and Regular refuses to boot on an older Lite:
# Regular, lite.lock (comments trimmed)
ref=1.0.18
sha=26403382e6cdb667ecef28305f0560a46361ea65
version=1.0.18
If a store also has the WordPress.org copy of Lite active, Regular defers to whichever copy loaded first and switches the redundant standalone copy off. We dropped compatibility with older Lite versions on purpose, so each Regular release runs on one known Lite and we test one pair instead of a matrix.
The words matter too
We also went through Lite's comments, constants and strings for lock wording and took all of it out. LOCKED_RENTAL_TYPES is gone, and so is a comment saying the add-on opens up the rest. Lite now mentions the paid editions in one place: an Upgrade page with a table of what each edition includes. Regular hides that page for its own customers through the sibooking_show_upgrade_page filter.
We resubmitted 1.0.9 the next day. Lite has been on WordPress.org since October 1, 2026.
What is allowed instead
Guideline 5 does not forbid making money from a plugin. It forbids one shape of it. These are fine:
- A free plugin that is complete on its own. Every screen it shows works, with no ceiling that a payment lifts.
- A separate paid plugin, hosted outside WordPress.org, that adds its own code through hooks the free plugin offers.
- Pointing people to the paid plugin, within the limits of guideline 11. Keep upgrade prompts on the plugin's own pages and make site-wide notices dismissible.
- A paid external service that does real work (guideline 6). A service whose only job is to check a licence key while the features sit in the plugin is not allowed.
A short way to tell a gate from an extension point: a gate takes away something the free plugin has. An extension point lets another plugin add something the free plugin doesn't have.
A checklist before you submit
- Grep the free plugin for filters that default to
falseand that only your paid plugin sets totrue. Each one is a likely finding. - Grep for numbers your paid plugin raises: days, items, rows, sites. A hard-coded limit with a paid branch is a quota.
- Grep the words: "locked", "pro only", "premium", "upgrade to". Rename what is honest and delete what is not.
- Treat hidden as shipped. If the code is in the zip, it counts, whatever the screen shows.
- Turn gates into registries that add. The free plugin's entries are complete, and other plugins append their own entries with their own code.
- Give stored data a plain fallback that carries no paid logic.
- Keep the upsell in one place, and keep it small.
- Install the free plugin alone on a clean site, open every screen, and ask whether everything there works with no ceiling.
- Fix every instance before you resubmit, not only the example in the email. The review team said plainly there would be no further round.
We ran Plugin Check on the new build as well. It covers a lot of the review list, but trialware is a judgement a person makes by reading the code, so read yours the same way before someone else does.
The free WooCommerce rental plugin page describes what Lite does on its own, and its Lite vs Regular vs Pro table lists what each edition adds. The paid editions are on the Rentals & Bookings for WooCommerce page. Everything that moved is listed in the Lite changelog (version 1.0.9) and the Regular changelog (version 1.0.74).


Top comments (0)