Skip to content

Closes #8700 Story 2 — Toggle-driven Content Delivery UI - #8732

Merged
hellofromahmed merged 29 commits into
enhancement/8693-rocketcdn-free-tiers-refactorfrom
feat/8700-toggle-ui-content
Sep 1, 2026
Merged

hellofromahmed merged 29 commits into
enhancement/8693-rocketcdn-free-tiers-refactorfrom
feat/8700-toggle-ui-content

Conversation

@Khadreal

@Khadreal Khadreal commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #8700

Replaces the old "select a tab = activate" model for Content Delivery (CDN) with explicit on/off toggles per mode (RocketCDN Free, RocketCDN Paid, Your own CDN/BYOCDN), decoupled from tab navigation. A user can now browse any CDN tab without activating or deactivating anything, and switching a mode on directly turns the previously active mode off. Along the way this also fixes a real bug uncovered during review: RocketCDN Free/Paid could never be turned back on once switched off, because the REST endpoint and the toggle checkbox both used a "should this look disabled" check that always evaluated true right before any activation attempt.

Type of change

  • New feature (non-breaking change which adds functionality).
  • Bug fix (non-breaking change which fixes an issue).
  • Enhancement (non-breaking change which improves an existing functionality).
  • Breaking change (fix or feature that would cause existing functionality to not work as before).
  • Sub-task of #(issue number)
  • Chore
  • Release

Detailed scenario

What was tested

All scenarios below were validated both via live browser interaction (Chrome, real toggle clicks) and via direct REST calls (WP-CLI rest_do_request() against the real REST controller, run in isolated processes to avoid stale in-memory option caching), across two full review-and-fix rounds plus a final CI run:

  • Tab switch causes no side effects — clicking an inactive tab only changes the visible panel; no REST call fires, no option changes. Verified live.
  • Mutual exclusivity — toggling one mode on automatically switches the previously active mode off (both client-side UI and the persisted cdn_state/cdn/cdn_type options). Verified live and via REST.
  • "Nothing active" is directly reachable — turning off the only active toggle reaches cdn_state = nothing with no mode forced back on. Verified live and via REST.
  • Active-mode visual cue — the "Active" indicator now renders on the tab itself (not just the section body) and updates live on toggle without a page reload. Verified live on both the RocketCDN and "Your CDN" tabs.
  • RocketCDN Free/Paid activation from "nothing active" — reproduced the original bug (403 on every attempt), fixed it, and confirmed activation now succeeds (200) with no regression to BYOCDN activation, idempotent re-activation, or the existing paid-tier entitlement check. Confirmed both the REST endpoint and the toggle checkbox's disabled state agree (a fix round found the checkbox still disabled after only the REST side was corrected).
  • Free-inactive activation prompt on "add page" — verified all four states via REST: no mode active → auto-activates Free silently, page added; Free already active → adds normally; another mode active without confirmation → 409 with the current mode named, page not written to the DB; another mode active with confirmation → activates Free and adds the page. Also confirmed the mode switch never happens before the page is actually validated and persisted (no dangling mode change on a failed add-page request).
  • One.com hosting compatibility — confirmed the restored disable_cdn_mode_toggle() correctly disables all three mode toggles when One.com's own CDN handling is active, and that this doesn't regress anything else in the One.com compatibility layer (CNAME, zone, varnish, tab visibility).
  • Automated: tests/Unit (CDN + OneCom groups) and tests/Integration (RocketCDN group) — all green in CI across PHP 7.4–8.5. PHPCS and PHPStan lint clean.

How to test

  1. Go to WP Rocket → Settings → Content Delivery with an active WP Rocket license and RocketCDN subscription.
  2. Click between the "RocketCDN" and "Your CDN" tabs — confirm nothing activates/deactivates just from viewing a tab.
  3. Toggle "RocketCDN Free" on — confirm "Your CDN" toggles off automatically, the "Active" label appears on the RocketCDN tab, and the mode persists after a page reload.
  4. Toggle it back off — confirm "nothing active" is reachable (no mode gets force-selected).
  5. From "nothing active", toggle RocketCDN Free on again — this previously failed silently; confirm it now succeeds.
  6. With "Your CDN" active, use "ADD PAGE"/"ADD HOMEPAGE" under RocketCDN Free — confirm a browser confirmation prompt appears before switching modes; accepting it activates Free and adds the page, cancelling leaves everything unchanged.
  7. From "nothing active", add a page directly — confirm Free auto-activates with no prompt.
  8. On a site simulating One.com hosting (vcaching constant + oc_cdn_enabled option), confirm all three mode toggles render disabled.

Affected Features & Quality Assurance Scope

  • WP Rocket Settings → Content Delivery Network (CDN) tab: driver tabs, RocketCDN Free/Paid sections, "Your own CDN" (BYOCDN) section, status indicator.
  • RocketCDN REST API: /wp-rocket/v1/rocketcdn/mode, /wp-rocket/v1/rocketcdn/pages, /wp-rocket/v1/rocketcdn/pages/homepage.
  • One.com hosting compatibility subscriber (inc/ThirdParty/Hostings/OneCom.php).
  • No impact expected outside the CDN settings page and its REST endpoints.

Technical description

Documentation

  • Controller::should_disable_element_for_rocketcdn() (existing, unchanged) stays the "should this look disabled" check used for rendering (purge button, exclusions, etc.) — it legitimately includes the current pause state.
  • A new Controller::should_reject_rocketcdn_activation() is the same check without the pause-state condition, since a request to activate is by definition made while paused. It's now the single source of truth for both the /mode REST gate and the mode toggle's disabled attribute, so the two can never disagree again.
  • Rest::add_page()/add_homepage() gained an activation-prompt flow: the mode-switch decision is made early (fail-fast on bad entitlement or on "needs confirmation"), but the actual mode change is deferred until after the page is validated and successfully persisted, via a shared apply_cdn_mode() helper (also used by save_cdn_mode()).
  • The active-mode tab indicator is driven by Context::get_applied_cdn_state() (the real persisted mode), kept distinct from the "currently viewed" tab state (cdn_type) — these were previously conflated under the same CSS class.
  • One.com's disable_cdn_mode_toggle() (renamed from disable_cdn_pause_option(), which disabled a pause button this PR removes) hooks the same rocket_cdn_driver_sections filter at the same priority, now targeting is_forced_off on all three mode sections instead of a status-indicator flag that no longer has a consumer.

New dependencies

None.

Risks

  • Mode-switch-before-validation was considered and mitigated: the CDN mode is only switched after the page-add request is confirmed persisted, so a failed request (bad URL, page limit reached, duplicate, subscription creation failure) never leaves the CDN mode silently changed.
  • Entitlement bypass was considered and mitigated: add_page()'s activation path uses the identical entitlement gate save_cdn_mode() enforces, so a site without a valid license/active subscription can't get RocketCDN Free force-activated by adding a page.
  • One.com regression: restoring/renaming the compatibility hook was reviewed in isolation to confirm filter-priority ordering still guarantees it overrides the core logic, and that it mirrors the same UI-only (non-REST-enforced) characteristic the original pause-button guard had — not a new gap.
  • No new external dependencies or API surface beyond the one new optional REST parameter (confirm_activation, boolean, sanitized via rest_sanitize_boolean).

Mandatory Checklist

Code validation

  • I validated all the Acceptance Criteria. If possible, provide screenshots or videos.
  • I triggered all changed lines of code at least once without new errors/warnings/notices.
  • I implemented built-in tests to cover the new/changed code.

Code style

  • I wrote a self-explanatory code about what it does.
  • I protected entry points against unexpected inputs.
  • I did not introduce unnecessary complexity.
  • Output messages (errors, notices, logs) are explicit enough for users to understand the issue and are actionnable.

Unticked items justification

N/A — all mandatory items above were completed.

Additional Checks

  • In the case of complex code, I wrote comments to explain it.
  • When possible, I prepared ways to observe the implemented system (logs, data, etc.).
  • I added error handling logic when using functions that could throw errors (HTTP/API request, filesystem, etc.)

@codacy-production

codacy-production Bot commented Aug 18, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Coverage 86.47% diff coverage · +0.10% coverage variation

Metric Results
Coverage variation +0.10% coverage variation (-0.10%)
Diff coverage 86.47% diff coverage (50.00%)

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (00f42ce) 47766 22536 47.18%
Head commit (96a1496) 47853 (+87) 22625 (+89) 47.28% (+0.10%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#8732) 133 115 86.47%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@hellofromahmed
hellofromahmed changed the base branch from develop to enhancement/8693-rocketcdn-free-tiers-refactor August 18, 2026 17:58
@Khadreal Khadreal self-assigned this Aug 19, 2026
@Khadreal
Khadreal marked this pull request as ready for review August 19, 2026 14:52
@Khadreal
Khadreal requested a review from a team August 20, 2026 09:50
@Khadreal

Copy link
Copy Markdown
Contributor Author

Screenshot

Screenshot 2026-08-20 at 11 26 08 Screenshot 2026-08-20 at 11 26 30

@jeawhanlee jeawhanlee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Job, Sir

Comment thread inc/Engine/Admin/Settings/Page.php Outdated
Comment thread inc/Engine/CDN/RocketCDN/Rest.php Outdated
Comment thread views/settings/sections/cdn/rocketcdn-free.php Outdated
Comment thread views/settings/sections/cdn/rocketcdn-paid.php Outdated
Comment thread views/settings/sections/cdn/your-own-cdn.php Outdated
@Khadreal
Khadreal requested a review from jeawhanlee August 21, 2026 08:09
@Mai-Saad

Mai-Saad commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

@Khadreal Thanks for the PR, Here are the possible risks raised by Claude, please feel free to validate if we need further change(s) or GH or we are good 🙏

  1. Likely bootstrap deadlock for new sites (functional regression, worth verifying)
    The toggle's disabled attribute is driven by should_disable_element_for_rocketcdn(), which is true whenever is_cdn_paused() (! (bool) options->get('cdn')) is true — and cdn defaults to 0/off for a fresh install (inc/admin/upgrader.php:125, DeactivationIntent.php:141). Nothing in add_page/add_homepage sets cdn to 1 — only save_cdn_mode itself does that. So a first-time user's toggle could render permanently disabled, and the only way to enable cdn is the toggle call it's blocking. Notably, the PR's own integration test has to explicitly force this via add_filter('pre_get_rocket_option_cdn', '__return_true', ...) with the comment "simulate both so activation isn't rejected" (SaveCdnMode.php:45-61) — strong evidence this gate is real. Worth confirming whether the base branch (8693) initializes cdn=1 on install for the new state model; if not, this blocks free-tier activation for new users.

  2. Toggle doesn't get re-enabled after the intended unlock flow, without a page reload
    disabled is only computed server-side at render time. After a successful "Add Homepage"/"Add Page" call (which creates the RocketCDN subscription async via Action Scheduler), nothing in cdn-driver.js clears disabled on .wpr-cdn-mode-toggle__input — updateRocketCDNElementsState only toggles a CSS class on unrelated sections. Users must manually refresh the settings page before they can flip the toggle.

  3. Race condition in the toggle change handler
    initCdnModeToggle fires a fetch immediately on every change event with no debounce, disabling-during-request, or request sequencing. Rapid clicks across toggles can resolve out of order, and previouslyActive is derived from a DOM class (wpr-cdn-active-indicator) that's optimistically mutated before prior requests settle — so failure rollback can restore the wrong toggle. Also, on failure only toggleDriverSections() is rolled back for the previous toggle; setActiveTab() (tab highlight, driver label, help link) isn't, so the tab/label UI can end up inconsistent with the actual saved CDN mode.

  4. Missing negative test for the 403 "forced off" rejection
    SaveCdnMode.php fixture covers happy paths, invalid mode, and unauthenticated access, but never asserts that activating rocketcdn_free/rocketcdn_paid while forced-off actually returns the cdn_mode_forced_off 403 (Rest.php:528-551) — the setup deliberately avoids that branch instead of testing it.

  5. Accessibility: unnamed toggle controls
    The new elements (e.g. rocketcdn-free.php:32-42) are wrapped only by a containing an empty slider — no aria-label/aria-labelledby ties the visible "Free"/"Pro"/title text to the control, so screen readers announce three unnamed checkboxes.

Comment thread src/js/global/cdn-driver.js
Comment thread inc/Engine/CDN/RocketCDN/Rest.php
Comment thread tests/Integration/inc/Engine/CDN/RocketCDN/Rest/SaveCdnMode.php
@hellofromahmed

hellofromahmed commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Note

Generated by the AI delivery pipeline (lead-reviewer · Claude Sonnet 5). Re-review after fix commits (9fdde98, 74761ec merge, 3043fdb).

Review: ❌ CHANGES REQUESTED

Resolved since last review:

  • Pause/resume removal confirmed intentional by the author on this PR's own thread ("...this is intentional, the toggle (activate/deactivate) is now in place of the old pause and resume"), and the backend was fully finished off in 3043fdb (/pause route + save_pause_state() deleted entirely, not just the UI button).
  • save_cdn_mode() now rejects mode=rocketcdn_paid with 403 cdn_mode_paid_subscription_required unless subscription_controller->is_paid(), with a new integration test shouldRejectPaidModeWhenSubscriptionIsNotPaid covering it.
  • The deleted SaveState.php test is no longer a coverage gap — save_pause_state() and its route are gone, so there's nothing left to test.
  • Legacy cdn / cdn_type options (read directly by inc/functions/htaccess.php:703 and inc/Addon/WebP/AbstractWebp.php:93) are now written synchronously inside save_cdn_mode() (9fdde98), verified by the integration test asserting cdn/cdn_type per mode. Traced the read path: Options_Data::get() applies pre_get_rocket_option_{key}, and CdnStateBridge::resolve_live() (from the merged base branch) recomputes cdn_state live from the same legacy fields, so front-end consumers and stored state stay consistent.

Remaining blockers:

  • [MEDIUM] AC5 (RocketCDN Free activation prompt on save, with auto-activate for the very first page when nothing is active) — still entirely unaddressed. Rest::add_page() is untouched by any commit on this branch; no prompt/auto-activate logic exists anywhere in PHP or JS.
  • [MEDIUM] inc/ThirdParty/Hostings/OneCom.php:123 disable_cdn_pause_option() still sets status_indicator['disable_pause_btn'], which is now unambiguously dead (the backend pause route is gone too, not just the button). Untouched by the fix commits.
  • [MEDIUM] AC3 — the "active mode" visual cue is still only on the section header (.wpr-optionHeader / wpr-cdn-active-indicator, see src/js/global/cdn-driver.js:updateCdnActiveIndicator), not on the tab itself (views/settings/partials/cdn/cdn-driver-tabs.php). wpr-cdn-tabs__tab--active only reflects which tab is currently viewed, not whether that mode is actually active — confirmed unchanged by the latest commits. Per the issue's literal wording ("the tab name carries the visual cue"), this AC is Partial, not Done.

Nice-to-haves:

  • views/settings/partials/cdn/cdn-status-indicator.php$rocket_is_pause_btn_disabled / $rocket_hide_pause_btn are still unused dead variables.
  • tests/Unit/inc/Engine/CDN/Context/getCdnState.php (and siblings) still don't exercise the $cdn_state override parameter directly — only indirect coverage via the REST integration test.
  • Rest::save_cdn_mode() now writes cdn_state directly, but pre_get_rocket_option_cdn_state (CdnStateBridge::resolve_live) always recomputes and overrides it on read — the direct write is effectively inert for reads. Works today because the two computations agree, but it's two sources of truth reconciled only by filter precedence; worth a comment or simplification in a follow-up.

@hellofromahmed

Copy link
Copy Markdown
Contributor

Note

Generated by the AI delivery pipeline (orchestrator · Claude Sonnet 5). Full consolidated findings from Lead Review + QA, including the re-review after commits 3043fdb ("review modifications") and 9fdde98 ("Add bridge option to save cdn mode"). The individual lead-review and QA comments above are kept short by design — this comment has the complete detail behind them.

Status: ❌ Not yet mergeable — CHANGES REQUESTED (lead review) + FAIL (QA)


✅ Resolved since the first review pass

Item Resolution
PAUSE/RESUME CDN feature silently removed Confirmed intentional by the author on the PR thread; removal completed end-to-end in 3043fdb (route + save_pause_state() handler deleted, not just the UI)
save_cdn_mode() accepted mode=rocketcdn_paid with no entitlement check is_paid() check added; returns 403 cdn_mode_paid_subscription_required; covered by new test shouldRejectPaidModeWhenSubscriptionIsNotPaid
Deleted test for save_pause_state() Moot — the method itself no longer exists
Legacy cdn/cdn_type options not updated by the new toggle (CdnStateBridge.php was deleted, nothing replaced its wiring) 9fdde98 added a bridge in save_cdn_mode() that writes both legacy options directly. QA verified this live via WP-CLI option reads before/after a toggle call — htaccess.php and AbstractWebp.php now see the correct state.
Unrelated rocket_thank_you_license() function added to inc/admin/ui/notices.php Removed

❌ Still blocking

1. [MUST_HAVE — new, QA] RocketCDN Free/Paid can never be activated from "nothing active"

  • Evidence: POST /wp-rocket/v1/rocketcdn/mode {"mode":"rocketcdn_free"} from a cdn=0 state returns 403 cdn_mode_forced_off every time — reproduced 3× via WP-CLI rest_do_request() in isolated processes, bypassing only the nonce/cookie layer (no wp-config.php edits, no faked license).
  • Root cause: Controller::should_disable_element_for_rocketcdn() (inc/Engine/CDN/Render/Controller.php:644) calls is_cdn_paused(), which reads the pre-write cdn option — evaluated before save_cdn_mode() (inc/Engine/CDN/RocketCDN/Rest.php) persists the new state. Read-before-write ordering bug. BYOCDN is unaffected (not in the guarded mode list); only RocketCDN Free/Paid activation is blocked.
  • Why CI didn't catch it: the new integration test tests/Integration/inc/Engine/CDN/RocketCDN/Rest/SaveCdnMode.php force-overrides the pre_get_rocket_option_cdn filter to always return true in setUp()/tearDown(). That masks exactly this bug — CI is green, but the real user-facing activation flow is broken.
  • Fix: reorder the write/guard-check so should_disable_element_for_rocketcdn() evaluates against the state save_cdn_mode() is about to persist (or re-read cdn after the write), and stop mocking pre_get_rocket_option_cdn in the test — assert against the real option instead.

2. [MEDIUM] AC5 — Free-inactive activation prompt / auto-activate on first page — not implemented

  • Issue's acceptance criterion: Given RocketCDN Free is inactive, when the user adds a page and saves, they are prompted to activate Free — except when no mode is active at all, in which case the first page auto-activates Free without a prompt.
  • Evidence: add_page()/add_homepage() in inc/Engine/CDN/RocketCDN/Rest.php and their JS counterparts (initAddPage()/initAddHomepage() in src/js/global/cdn-driver.js) are untouched by this PR's diff (confirmed both in the original review and the re-review). No confirm/prompt/auto-activate logic exists anywhere in either JS or PHP.
  • Open question: unclear whether this is meant to land in a later PR of epic Epic: RocketCDN Free Tiers refactor — unify RocketCDN Free/Pro and Other CDN under toggle-driven modes #8693 — could not confirm from this branch alone. Needs a decision either way before this PR can close out AC5.

3. [MEDIUM] AC3 — active-mode visual cue is on the section header, not the tab

  • Issue's acceptance criterion: Given a mode is active, when its tab is rendered, then the tab name carries the visual cue.
  • Evidence: views/settings/partials/cdn/cdn-driver-tabs.php has zero diff against the base branch in either commit round. The "Active" indicator (.wpr-cdn-active-indicator / "Active" label) was added only to each section's .wpr-optionHeader (rocketcdn-free.php, rocketcdn-paid.php, your-own-cdn.php). The tab's --active CSS class reflects which tab is currently viewed, not which mode is enabled — these are two different states conflated under the same class name.
  • Fix: add a distinct indicator on the tab itself, driven by cdn_state (not by click/view state), so the tab name — not just the section body — carries the cue per the issue's literal wording. (Or: get explicit product sign-off that the section-header placement satisfies intent, and update the issue text to match.)

4. [MEDIUM] inc/ThirdParty/Hostings/OneCom.php:123 — dead pause-button guard

  • Evidence: disable_cdn_pause_option() sets status_indicator['disable_pause_btn'], which has had zero consumer since both the pause-button markup and its backend route were removed in 3043fdb. The existing OneCom test only asserts the array value, so this regression is invisible to CI.
  • Fix: remove disable_cdn_pause_option() and its hook registration, or repurpose it against the new toggle model if OneCom hosting still needs to disable mode-switching for its customers.

⚠️ Test coverage gaps (AC6)

Nice-to-haves (non-blocking)

  • $rocket_is_pause_btn_disabled / $rocket_hide_pause_btn — unused dead template variables in cdn-status-indicator.php after the pause markup removal.
  • No dedicated Context unit test exercises the new $cdn_state override parameter on get_cdn_state() / get_applied_cdn_state() / get_rocketcdn_state().
  • save_cdn_mode()'s direct cdn_state option write is functionally inert on read (CdnStateBridge::resolve_live() always overrides it via the pre_get_rocket_option_cdn_state filter) — two sources of truth reconciled only by filter precedence; works today but is fragile.

Environment note (QA)

Both QA passes hit the same local guard: rocket_valid_key() (inc/functions/options.php:417) gates the full settings page render on this license-less local install, and WP_ROCKET_KEY/WP_ROCKET_EMAIL are pre-defined as empty constants, so it can't be unlocked via safe options-only means. All live verification was done instead via WP-CLI rest_do_request() directly against the REST controller (bypassing only the nonce/cookie layer, not the guard) — no wp-config.php edits were made in this run.

…e.com toggle compatibility

- Split should_disable_element_for_rocketcdn() into a dedicated
  should_reject_rocketcdn_activation() that excludes the pause-state check:
  a request to activate RocketCDN is by definition made while CDN is
  currently paused, so including that check rejected every first
  activation. Use the new method for both the /mode REST gate and the
  mode toggle's disabled state (the checkbox was still HTML-disabled even
  after the REST endpoint was fixed to accept the request).
- Add the Free activation-prompt / auto-activate flow to add_page() and
  add_homepage(): silently activates Free when no mode is active at all,
  asks for confirmation when another mode is active, applies the same
  entitlement gate as /mode, and only switches the mode once the page is
  confirmed persisted (never on a request that fails validation).
- Add the active-mode visual cue to the CDN driver tabs themselves
  (previously only shown on the section header).
- Restore One.com's CDN-toggle-disable compatibility hook, adapted from
  the removed pause button to the new toggle model
  (disable_cdn_mode_toggle(), targeting is_forced_off instead of the
  deleted pause button's status flag) so One.com hosting can still
  prevent the mode toggle from being switched while its own CDN handles
  delivery.
- Remove the now fully dead pause-button leftovers (OneCom's original
  hook + its 3 tests, and unused status-indicator template variables).
@hellofromahmed

Copy link
Copy Markdown
Contributor

Note

Generated by the AI delivery pipeline (orchestrator · Claude Sonnet 5). Summary of the fix round following the earlier consolidated review comment, now pushed in commit 95d37728f.

Status: ✅ All findings from this review round fixed and verified


What changed in this commit

  1. RocketCDN activation gate fixed (the core reported bug)Controller::should_disable_element_for_rocketcdn() was being reused as the /mode REST activation gate, but it includes is_cdn_paused(), which is always true immediately before any first activation — so RocketCDN Free/Paid could never be turned on from a "nothing active" state. Added a dedicated should_reject_rocketcdn_activation() that excludes that check, used for both the REST gate and the mode toggle checkbox's disabled state (the checkbox was still wrongly disabled even after the REST endpoint alone was fixed — found and confirmed by an extra QA pass).

  2. AC5 — Free activation-prompt / auto-activate flow implemented in add_page()/add_homepage():

    • No mode active at all → silently activates Free, adds the page, no prompt.
    • Another mode active → returns 409 rocketcdn_free_inactive_confirm_required naming the current mode; the frontend shows a confirmation dialog and retries with confirm_activation: true if accepted.
    • Applies the same entitlement gate as /mode (a site with an expired license/no active subscription can't get Free force-activated just by adding a page).
    • The mode switch only happens after the page is confirmed persisted — never left switched on a request that fails page-limit/URL/duplicate/subscription validation.
  3. AC3 — active-mode visual cue moved to the tab itself, not just the section header, updated live via JS on toggle without a page reload.

  4. One.com hosting compatibility — restored, not dropped. The old disable_cdn_pause_option() disabled a PAUSE button that this PR removed entirely, making it dead code — deleted along with the removed button in an earlier commit. Per explicit request, this was restored and adapted to the new toggle model instead of left removed: renamed to disable_cdn_mode_toggle(), it now disables the mode toggle checkbox itself (is_forced_off) for all three CDN sections (BYOCDN, RocketCDN Free, RocketCDN Paid) when One.com's own CDN handling is active, so users on One.com hosting still can't switch WP Rocket's CDN modes out from under it. BYOCDN's toggle previously had no disable-gate at all — added one so One.com's guard applies consistently across all three modes. Restored with updated test coverage (18/18 unit tests pass).

  5. Removed the now fully-dead pause-button leftovers (unused SCSS-adjacent template vars) that remained after the button itself was removed.


Verification this round

  • Lead review: 2 passes — first found the entitlement-gate bypass and missing-rollback issues in the initial AC5 implementation (both fixed), second (scoped to the One.com restoration) passed clean with only 2 cosmetic nice-to-haves, both applied.
  • QA: 3 passes — first confirmed the REST-layer fix and full AC5 flow via live REST calls (including verifying the mode switch never happens before the page is persisted); found the checkbox-still-disabled gap live in the browser; second pass confirmed that fix end-to-end (checkbox now clickable, click activates Free, tab indicator updates correctly on the RocketCDN tab).
  • Unit tests: 98/98 (CDN group), 18/18 (OneCom group). PHPCS: zero new violations introduced. php -l clean on all touched files.
  • Integration test suite (Test_AddPage/Test_AddHomepage) could not be confirmed in this sandbox — wp_get_environment_type() === 'local' blocks every test in those two classes uniformly (pre-existing sandbox default, affects old and new assertions identically, not a regression from this PR). The equivalent scenarios were independently verified via direct REST calls instead.

No outstanding blockers from either gate as of this push.

hellofromahmed and others added 6 commits August 31, 2026 23:31
…est's Options_Data instance

The new AC5 integration tests set cdn_state directly through the shared
'options' service before making the request, assuming it was the same
Options_Data instance Rest::add_page() reads from. In CI it isn't (or
isn't reliably): the pre-set was invisible to add_page(), so cdn_state
always read back as whatever the DB row already held, and the three new
scenarios exercised the wrong branch.

Switched to the same pre_get_rocket_option_cdn_state live filter override
technique tests/Integration/.../SaveCdnMode.php already used for the
analogous 'cdn' key — this is read on every Options_Data::get() call
regardless of which instance holds it, so it reliably reaches
add_page()'s own read. Removed the now-meaningless cdn_state assertion
on the "already active, no-op" case, since nothing ever persists that
value to the DB in that scenario (only the live read is faked).

Also simplified add_page()'s three-way if/elseif/else down to a single
guard clause (same behavior, no more redundant else), per a Codacy
notice on the pushed commit.
A recent commit ("return cdn_state when it's saved into DB") changed
CdnStateBridge to only recompute cdn_state from the legacy cdn/cdn_type
fields when no value was already stored, and renamed its filter hook
from pre_get_rocket_option_cdn_state to get_rocket_option_cdn_state.

Both changes broke the class's actual, tested contract: cdn_state must
always be resolved live from cdn/cdn_type (and live subscription state)
on every read, never trusted from whatever was last written — this is
what lets the bridge correctly reflect cdn/cdn_type changes made outside
apply_cdn_mode() (other settings paths, forced-pause, hosting compat
filters like OneCom's) and what tests/Integration/.../CdnStateBridge/
resolveLive.php already asserts. The hook rename also meant the bridge
silently stopped running at all for any site where cdn_state was never
explicitly stored (legacy users upgrading, the bridge's primary use
case), since get_rocket_option_* only fires when the key already exists.

Reverted both changes back to the original, tested design.
@hellofromahmed
hellofromahmed merged commit 91a0742 into enhancement/8693-rocketcdn-free-tiers-refactor Sep 1, 2026
13 checks passed
@hellofromahmed
hellofromahmed deleted the feat/8700-toggle-ui-content branch September 1, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Story 2 — Toggle-driven Content Delivery UI

4 participants