Orangecat_Visibility

June 2, 2026 · View on GitHub

Guest access controls for Magento 2 B2B storefronts — force login redirect with URL whitelisting and selective price/cart hiding for guests.

Module: Orangecat_Visibility Version: 1.0.0 License: OSL-3.0 Author: Oliverio Gombert olivertar@gmail.com


Table of Contents

  1. Overview
  2. Theme Compatibility
  3. Requirements
  4. Installation
  5. What Gets Installed
  6. Configuration
  7. Store Admin Guide
  8. Guest Experience
  9. Developer Guide
  10. REST API
  11. Frontend Routes Reference
  12. DevOps & Integrator Notes

Overview

Orangecat_Visibility controls what unauthenticated (guest) visitors can see and access on a B2B storefront. It provides two independent, non-overlapping operating modes that can be enabled separately.

The module handles:

  • Force Login mode — redirect guests away from any page not on a URL whitelist, preserving the original URL so the visitor lands back on it after login.
  • Catalog Visibility mode — when Force Login is off, selectively hide product prices and/or the Add to Cart button for guests without blocking navigation.
  • URL Whitelist — a per-store, admin-managed table of URL rules with pluggable matching strategies (static exact match or regex). System-critical URLs (login, logout, forgot password, etc.) are pre-seeded and protected from accidental deletion.
  • Server-side cart protection — blocks guest POST requests to the Add to Cart controller even when the button is CSS-hidden, preventing bypass via direct HTTP calls.
  • Post-login redirect — after the guest authenticates, the browser returns to the page they originally requested.

Position in the Orangecat B2B Dependency Chain

This module is standalone and does not depend on Orangecat_Company. It can be installed independently on any Magento 2 storefront.

Orangecat_Core (via composer: orangecat/core)
  └── Orangecat_Visibility     ← this module (standalone)

Operating Modes

ModeKey ConfigWhat It Does
Force Loginvisibility/general/enabled = 1Intercepts every frontend request; redirects guests unless the URL matches a whitelist rule.
Catalog Visibilityvisibility/general/enabled = 0Force Login off; hide_price and/or hide_add_to_cart flags control catalog display for guests.

The two modes are mutually exclusive by design: when Force Login is active, the catalog visibility flags have no effect (guests are redirected before seeing any catalog content).


Theme Compatibility

ThemeStatusNotes
LumaSupportedAll plugins fully apply. Block interceptors target standard Magento catalog block classes present in Luma.
HyväPartialForce Login and pricing model plugins (return 0) work. PriceBoxPlugin and AmountPlugin work only if Hyvä still delegates to Magento\Framework\Pricing\Render\PriceBox/Amount. CatalogBlockPlugin may miss Hyvä Alpine.js listing components that do not extend the targeted block classes.
Breeze EvolutionPartialForce Login works. Pricing plugins work. CatalogBlockPlugin targets Magento core block classes; Breeze-specific listing components may need additional interceptors. The minicart hide (minicart-wrapper class injection) may require verification.

For full Hyvä/Breeze Catalog Visibility support, add dedicated plugins targeting the theme-specific block classes and register them under the appropriate area in etc/frontend/di.xml. The VisibilityChecker service is theme-agnostic and can be injected directly.


Requirements

DependencyVersion / Notes
PHP>= 8.1
Magento2.4.x
Orangecat_Corecomposer: orangecat/core
Magento_Configcore
Magento_Customercore
Magento_Catalogcore
Magento_Checkoutcore
Magento_Salescore

Installation

# From repo root
git submodule add git@github.com:olivertar/m2_visibility.git app/code/Orangecat/Visibility
git submodule update --init --recursive

Enable the Module

Run inside the PHP container (reward shell):

bin/magento module:enable Orangecat_Visibility
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush

What Gets Installed

Database Tables

visibility_whitelist

Stores the URL rules used by the Force Login whitelist checker.

ColumnTypeNotes
rule_idsmallint, PKAuto-increment.
store_idsmallint, FKstore.store_id (CASCADE DELETE). 0 = all store views.
labelvarchar(255)Human-readable name. Unique constraint.
url_rulevarchar(255)URL pattern; interpreted by the selected strategy.
strategytextMatcher identifier: default (static) or regex.
editablebooleantrue = admin-editable. false = system-protected, no Edit/Delete in UI.

Indexes: store_id (btree), editable (btree), url_rule + store_id (btree).

EAV Attributes

None.

Data Patches

AddDefaultWhitelistRules

Inserts 21 pre-seeded whitelist rules on first setup:upgrade. Rules with editable = 0 are system-protected and cannot be deleted from the admin UI.

LabelURL RuleStrategyEditable
Rest API/restregexyes
Customer Account Login/customer/account/loginregexno
Customer Account Logout/customer/account/logoutregexno
Customer Account Logout Success/customer/account/logoutSuccessregexno
Customer Account Create/customer/account/createregexno
Customer Account Create Post/customer/account/createpoststaticno
Customer Account Create Password/customer/account/createPasswordregexno
Customer Account Forgot Password/customer/account/forgotpasswordregexno
Customer Account Forgot Password Post/customer/account/forgotpasswordpostregexno
Customer Account Reset Password Post/customer/account/resetpasswordpoststaticno
Customer Section Load/customer/section/loadregexno
Contact Us/contactregexyes
CMS Help/helpregexyes
Sitemap.xml/sitemap.xmlregexyes
Robots.txt/robots.txtregexyes
Varnish ESI url/page_cache/block/esi/blocksstaticno
Store-Switcher Redirect/stores/store/redirectstaticno
Store-Switcher Switch/stores/store/switchstaticno
Company Account Create/company/account/createregexno
Company Account Create Post/company/account/createpoststaticno
Company Account Success/company/account/successregexno

INSERT IGNORE is used so re-running setup:upgrade never duplicates existing rows.


Configuration

Path: Stores → Configuration → Orangecat → Visibility (B2B)

General Settings

LabelConfig PathDefaultDescription
Enable Redirect to Loginvisibility/general/enabledNoMaster switch for Force Login mode. When Yes, all guest requests are intercepted.
Target URLvisibility/general/target_url/customer/account/loginURL guests are redirected to. Only shown when Enable is Yes.
visibility/general/enabled
visibility/general/target_url

Catalog Visibility

These settings apply only when Force Login is disabled. They have no effect when visibility/general/enabled = 1.

LabelConfig PathDefaultDescription
Hide Price For Guestvisibility/catalog/hide_priceNoHide product prices for guests. Also suppresses <meta property="product:price:amount"> OG tags. When enabled, Add to Cart is hidden too (a guest who cannot see the price cannot buy).
Hide Add to Cart Button For Guestvisibility/catalog/hide_add_to_cartNoHide Add to Cart button and minicart for guests independently of the price setting.
visibility/catalog/hide_price
visibility/catalog/hide_add_to_cart

Store Admin Guide

Accessing Settings

  • Whitelist rules: Admin menu → Visibility → Whitelist
  • Module configuration: Admin menu → Visibility → Settings (or Stores → Configuration → Orangecat → Visibility (B2B))

Enabling Force Login

  1. Go to Stores → Configuration → Orangecat → Visibility (B2B).
  2. Under General Settings, set Enable Redirect to Login to Yes.
  3. Optionally change Target URL to a custom landing page (e.g., /company/account/create for a B2B registration-first flow).
  4. Save Config and flush cache.

Once active, every guest who reaches the Magento application layer and requests a URL not on the whitelist will be redirected to the target URL.

Enabling Catalog Visibility Restrictions

  1. Keep Enable Redirect to Login set to No.
  2. Under Catalog Visibility:
    • Set Hide Price For Guest to Yes to remove prices from the catalog for guests.
    • Set Hide Add to Cart Button For Guest to Yes to hide cart controls without hiding prices.
  3. Save Config and flush cache.

When Hide Price is Yes, the Add to Cart button is automatically also hidden, regardless of the Add to Cart toggle, because allowing purchase without a visible price is illogical.

Managing the Whitelist

Path: Admin menu → Visibility → Whitelist

The grid shows all rules with columns: ID, Label, URL Rule, Strategy, Store View. Rows with editable = false show no Edit or Delete action links.

Adding a Rule

  1. Click Add New Rule.
  2. Fill in:
    • Label — unique descriptive name (e.g., My Custom Page).
    • URL Rule — the URL path to allow (e.g., /about-us).
    • StrategyStatic Match for exact path comparison; Regex Match for pattern matching.
    • Store View — limit the rule to a specific store view, or leave as All Store Views.
  3. Click Save Rule.

Matching Strategies

StrategyIdentifierHow It Works
Static MatchdefaultExact path comparison (trailing slashes ignored): rtrim($path, '/') === rtrim($urlRule, '/').
Regex MatchregexPHP preg_match with the pattern #<url_rule>#iU (case-insensitive, ungreedy).

Regex example: URL Rule /product/view with Regex Match will match /product/view/id/42 and any other path containing that substring. To match exactly, anchor the pattern: ^/product/view$.

Deleting a Rule

System-protected rules (editable = 0) cannot be deleted from the UI. To remove them, delete the row directly in the database or via a custom data patch.


Guest Experience

This module has no dedicated frontend pages or templates. Its effect is entirely behavioral:

Force Login mode active

  • Any URL request that is not on the whitelist triggers an immediate HTTP redirect to the configured target URL (default: the customer login page).
  • After successful login, the guest is redirected back to the page they originally requested. The original URL is saved in CustomerSession::setBeforeAuthUrl() before the redirect.
  • AJAX requests are not redirected (preventing broken JSON responses).
  • Static asset file extensions (.css, .js, .png, .jpg, .gif, .svg) bypass the check.

Catalog Visibility mode active (Force Login off)

  • Hide Price: Product prices appear as $0.00 at the model level, and the rendered price boxes and amount containers are hidden via injected style="display:none;". The product:price:amount Open Graph meta tag is also stripped from the HTML.
  • Hide Add to Cart: The tocart form, Add to Cart button (by CSS class and by id="product-addtocart-button"), and the minicart wrapper are hidden via injected style="display:none;". Any direct POST to /checkout/cart/add by a guest is blocked server-side with an error message and a redirect to the login page.

Logged-in customers are never affected by either mode.


Developer Guide

Module Structure

Orangecat/Visibility/
├── Api/
│   ├── Data/WhitelistEntryInterface.php       # Data object contract
│   └── Repository/WhitelistRepositoryInterface.php  # getCollection() only
├── Block/Adminhtml/Whitelist/Edit/            # Form button providers
├── Controller/Adminhtml/Whitelist/            # Admin CRUD controllers
├── Model/
│   ├── ResourceModel/WhitelistEntry/          # Collection + Grid collection
│   ├── ResourceModel/WhitelistEntry.php       # DB resource model
│   ├── Source/Strategy.php                    # Option source for strategy dropdown
│   ├── Strategy/                              # URL matching strategy pattern
│   │   ├── MatcherInterface.php
│   │   ├── StaticMatcher.php                  # Exact path match
│   │   ├── RegexMatcher.php                   # preg_match with #pattern#iU
│   │   └── StrategyManager.php                # DI-injected strategy registry
│   ├── WhitelistEntry.php                     # Entity model
│   ├── WhitelistEntry/DataProvider.php        # UI form data provider
│   └── VisibilityChecker.php                  # Central logic service
├── Plugin/
│   ├── FrontControllerPlugin.php              # aroundDispatch — force login redirect
│   ├── AfterLoginPlugin.php                   # afterExecute LoginPost — restore referer
│   ├── PreventAddToCartPlugin.php             # aroundExecute Cart\Add — server-side block
│   ├── Block/CatalogBlockPlugin.php           # afterToHtml — hides tocart/minicart HTML
│   └── Pricing/
│       ├── FinalPricePlugin.php               # afterGetValue — returns 0 for guests
│       ├── RegularPricePlugin.php             # afterGetValue — returns 0 for guests
│       └── Render/
│           ├── AmountPlugin.php               # afterToHtml — hides zero-amount containers
│           └── PriceBoxPlugin.php             # afterToHtml — hides zero-price boxes
├── Repository/WhitelistRepository.php
├── Setup/Patch/Data/AddDefaultWhitelistRules.php
├── Ui/Component/Listing/Column/WhitelistActions.php
├── etc/
│   ├── acl.xml
│   ├── config.xml
│   ├── db_schema.xml
│   ├── di.xml                                 # Preferences + StrategyManager + grid collection
│   ├── module.xml
│   ├── adminhtml/
│   │   ├── menu.xml
│   │   ├── routes.xml
│   │   └── system.xml
│   └── frontend/
│       └── di.xml                             # All frontend plugins registered here
├── view/adminhtml/
│   ├── layout/                                # whitelist_index, _new, _edit
│   └── ui_component/                          # whitelist_listing, whitelist_form
└── registration.php

Key Classes

Model/VisibilityChecker

Central service injected by all catalog visibility plugins. Uses FPC-safe Magento\Framework\App\Http\Context (not CustomerSession) to determine guest status, making it compatible with full-page cache.

MethodReturnsDescription
isGuest(): boolboolTrue if CustomerContext::CONTEXT_AUTH is falsy in HTTP context.
isForceLoginEnabled(): boolboolReads visibility/general/enabled config flag.
shouldHidePrice(): boolboolTrue when Force Login is off AND guest AND hide_price = 1.
shouldHideAddToCart(): boolboolTrue when Force Login is off AND guest AND (hide_price = 1 OR hide_add_to_cart = 1).

Api/Data/WhitelistEntryInterface

ConstantFieldType
RULE_IDrule_idint
STORE_IDstore_idint
LABELlabelstring
URL_RULEurl_rulestring
STRATEGYstrategystring
EDITABLEeditablebool

Standard getters/setters for each field.

Api/Repository/WhitelistRepositoryInterface

getCollection(): Collection

Returns the full WhitelistEntry\Collection. There is no getById, save, or delete in the service contract — CRUD for whitelist rules is handled by the admin controllers directly via the model factory.

Model/Strategy/MatcherInterface

getName(): string
isMatch(string $path, WhitelistEntryInterface $rule): bool

All matchers implement this interface. Registered via StrategyManager in etc/di.xml:

<item name="default" xsi:type="object">Orangecat\Visibility\Model\Strategy\StaticMatcher</item>
<item name="regex"   xsi:type="object">Orangecat\Visibility\Model\Strategy\RegexMatcher</item>

Plugins

All frontend plugins are declared in etc/frontend/di.xml (frontend area only).

ClassTargetHookPurpose
FrontControllerPluginMagento\Framework\App\FrontControllerInterfacearoundChecks guest status; iterates whitelist; redirects if no rule matches. Skips AJAX and asset file paths.
AfterLoginPluginMagento\Customer\Controller\Account\LoginPostafterOverrides the post-login redirect to the referer URL if it is not the login page.
PreventAddToCartPluginMagento\Checkout\Controller\Cart\AddaroundBlocks guest add-to-cart POST requests when either Force Login or Hide Cart is enabled.
FinalPricePluginMagento\Catalog\Pricing\Price\FinalPriceafterReturns 0 for getValue() when shouldHidePrice(). Also applied to Magento\ConfigurableProduct\Pricing\Price\FinalPrice.
RegularPricePluginMagento\Catalog\Pricing\Price\RegularPriceafterReturns 0 for getValue() when shouldHidePrice().
PriceBoxPluginMagento\Framework\Pricing\Render\PriceBoxafterInjects style="display:none;" on the price-box container when the rendered amount is 0.0.
AmountPluginMagento\Framework\Pricing\Render\AmountafterInjects style="display:none;" on the price-container span when getDisplayValue() is 0.0.
CatalogBlockPluginMagento\Catalog\Block\Product\View, ListProduct, Related, Crosssell, Upsell, Magento\Checkout\Block\Cart\Sidebar, Magento\CatalogWidget\Block\Product\ProductsListafterInjects style="display:none;" into tocart elements, tocart-form forms, the PDP Add to Cart button, and the minicart wrapper. Removes product:price:amount meta tag when price is hidden.

Observers

This module registers no observers.

JS Components

This module provides no JavaScript components — all guest-restriction logic is server-side PHP.

Email Templates

This module sends no email.

ACL Resources

Resource IDTitleLocation
Orangecat_Visibility::baseVisibilityUnder Orangecat_Core::menu
Orangecat_Visibility::whitelistWhitelistUnder Visibility
Orangecat_Visibility::configProduct VisibilityUnder Stores → Settings → Configuration

Adding Custom Logic

  • Add a new URL matching strategy — implement Model\Strategy\MatcherInterface, then register it in etc/di.xml under the StrategyManager strategies array argument with a unique identifier key. It appears automatically in the admin Strategy dropdown via Model\Source\Strategy.
  • Extend catalog visibility to Hyvä/Breeze blocks — inject VisibilityChecker into a new plugin targeting the theme-specific block class; call shouldHidePrice() or shouldHideAddToCart() to apply the same logic. Register in etc/frontend/di.xml.
  • Add a third operating mode — add config fields to system.xml, add reader methods to VisibilityChecker, and wire new plugins; VisibilityChecker is the single point of truth for all guest-state checks.

REST API

This module exposes no REST API endpoints. There is no etc/webapi.xml file.


Frontend Routes Reference

This module has no frontend routes. There is no etc/frontend/routes.xml file. All guest-facing behavior is enforced through plugins on existing Magento controllers and rendering classes.


DevOps & Integrator Notes

Deployment Checklist

# After deploying or updating this module:
bin/magento module:enable Orangecat_Visibility
bin/magento setup:upgrade          # creates visibility_whitelist table + seeds default rules
bin/magento setup:di:compile       # regenerates interceptors for all registered plugins
bin/magento setup:static-content:deploy -f
bin/magento cache:flush

Integration Token Scope

This module manages frontend behavior only. No integration token permissions are required to interact with whitelist rules — all management is done via the Admin UI.

Minimum ACL permissions for admin users who need to manage the whitelist:

  • Orangecat_Visibility::whitelist
  • Orangecat_Visibility::config (for store configuration access)

Disabling Without Uninstalling

Set visibility/general/enabled = No and both catalog flags to No in store configuration. This disables all guest restrictions without removing any data.

bin/magento module:disable Orangecat_Visibility
bin/magento setup:upgrade
bin/magento cache:flush

Data Integrity

  • visibility_whitelist.store_id has a CASCADE DELETE constraint on store.store_id. Deleting a store view removes its specific whitelist rules. Rules with store_id = 0 (all store views) are unaffected.
  • The label column has a unique constraint. Duplicate labels are rejected at the database level.
  • The data patch uses INSERT IGNORE to be idempotent — safe to run setup:upgrade multiple times.
  • System-protected rules (editable = 0) are not deletable via the Admin UI, but can be removed directly in the database if needed. Removing them while Force Login is active may lock guests out of login or other essential flows.
  • No data is removed when the module is disabled. The visibility_whitelist table and its rows persist.