docs/plans/5.5-6.0-display-on-to-boolean.md
display_on → storefront_visible (PaymentMethod, ShippingMethod / DeliveryMethod)Status: 5.5 bridge shipped (storefront_visible accessor + Ransacker on the Spree::DisplayOn concern); 6.0 schema rename + concern drop pending
Target: 5.5 (API alias + alias_attribute shim ✓) + 6.0 (schema rename, drop the Spree::DisplayOn tri-state concern)
Depends on: 5.5-6.0-custom-fields-rename.md (same bridge pattern, already in flight). 6.0-fulfillment-and-delivery.md (drives the ShippingMethod → DeliveryMethod rename — this plan piggybacks on that wave).
Author: Damian + Claude
Last updated: 2026-05-20
Collapse the tri-state display_on enum (both / front_end / back_end) on Spree::PaymentMethod and Spree::ShippingMethod to a single boolean storefront_visible, mirroring what 5.5-6.0-custom-fields-rename.md does for MetafieldDefinition. The front_end-only state (visible to customers but hidden from admin) doesn't map to any real workflow — staff need to see every payment / shipping option to look up past transactions, process refunds, edit orders, etc. The remaining two states (both, back_end) collapse cleanly to a boolean.
Ships in two phases:
storefront_visible accessor (read + write) on both models that proxies to the existing display_on column. Admin API v3 speaks storefront_visible only on both reads and writes. The legacy Rails admin engine (spree/admin) keeps writing display_on via its form helpers — that path stays on the model layer until 6.0. SDK ships storefront_visible only. Admin SPA uses <StorefrontVisibleSwitch>. No schema changes.display_on string column with storefront_visible boolean on both tables. Remove the Spree::DisplayOn concern. Drop the display_on API alias.front_end-only doesn't reflect a real workflow — admin staff need visibility into every payment/shipping option for support, refunds, reconciliation, and manual order entry. Same reasoning as the custom-fields plan.storefront_visible not available_on_storefront. Matches the custom-fields plan exactly; one term for the same concept across the whole product.true. Most payment/shipping methods are customer-facing. back_end is the exception (manual check entry, internal wire-transfer methods, refund-only methods).storefront_visible only; writes still accept both names. The Admin API is the only consumer of display_on today (the admin SPA, which is next-tagged and pre-1.0); migrating the wire shape now is cheaper than carrying two read paths for six months. Writes stay permissive so any out-of-tree integration that sends display_on keeps working through the 5.5 cycle.Spree::DisplayOn concern in 6.0. The concern only had three callers (PaymentMethod, ShippingMethod, MetafieldDefinition); custom fields are already migrating off via 5.5-6.0-custom-fields-rename.md, so removing the concern entirely in 6.0 is a clean sweep.display_on keeps working; anyone reading storefront_visible gets the boolean.front_end-only edge case (if any merchant set it) maps to storefront_visible: true. Same as the custom-fields plan's data migration.Spree::PaymentMethod and Spree::ShippingMethod both:
display_on string column (values: 'both' | 'front_end' | 'back_end').include Spree::DisplayOn (concern at spree/core/app/models/concerns/spree/display_on.rb).display_on in their Admin API serializers (and Store API where applicable).Spree::DisplayOn concern provides:
scope :available (only both)scope :available_on_front_end (front_end + both)scope :available_on_back_end (back_end + both)validates :display_on, presence: true, inclusion: { in: DISPLAY.map(&:to_s) }available_on_front_end? instance predicatedisplay_on:string column → storefront_visible:boolean (default true, null: false).Spree::StorefrontVisible concern if more models want it later — defer until a third caller exists).storefront_visible:boolean. The display_on field is removed from the wire.alias_attribute shimMirrors 5.5-6.0-custom-fields-rename.md exactly. No schema changes.
Spree::DisplayOnSpree::PaymentMethod, Spree::ShippingMethod, and Spree::MetafieldDefinition all include Spree::DisplayOn today. We extend the existing concern with the new vocabulary so all three callers pick it up automatically — no per-model copy:
# spree/core/app/models/concerns/spree/display_on.rb (5.5)
module Spree
module DisplayOn
extend ActiveSupport::Concern
DISPLAY = [:both, :front_end, :back_end]
included do
scope :available, -> { where(display_on: [:both]) }
scope :available_on_front_end, -> { where(display_on: [:front_end, :both]) }
scope :available_on_back_end, -> { where(display_on: [:back_end, :both]) }
# New canonical scopes (5.5 → 6.0).
scope :storefront_visible, -> { where.not(display_on: 'back_end') }
scope :admin_only, -> { where(display_on: 'back_end') }
validates :display_on, presence: true, inclusion: { in: DISPLAY.map(&:to_s) }
def available_on_front_end?
display_on == 'front_end' || display_on == 'both'
end
# 5.5 bridge — `storefront_visible` is the canonical wire field.
# `back_end` → false; everything else → true.
def storefront_visible
display_on != 'back_end'
end
def storefront_visible=(value)
self.display_on = ActiveModel::Type::Boolean.new.cast(value) ? 'both' : 'back_end'
end
end
end
end
Reads: back_end → false, anything else (both or the legacy front_end) → true. Writes: true → both, false → back_end. Round-trip works.
Spree::MetafieldDefinition already defines storefront_visible accessors per 5.5-6.0-custom-fields-rename.md — those become redundant once the concern provides them and can be deleted. Coordinate via that plan's checklist.
In 6.0, the entire concern goes away — storefront_visible becomes a real column accessor on each model, scopes inline where needed (or, if a third caller wants it post-6.0, a smaller Spree::StorefrontVisible concern can be extracted).
Admin::PaymentMethodSerializer (and any future Admin::ShippingMethodSerializer):
# Before
typelize display_on: :string
attributes :display_on
# After (5.5) — wire field is the boolean only
typelize storefront_visible: :boolean
attributes :storefront_visible
display_on is dropped from the wire response. Reads-side consumers (admin SPA, SDK) must use storefront_visible. The DB column lives on under the hood — only the API surface changes.
Spree::PermittedAttributes.payment_method_attributes and .shipping_method_attributes keep both :display_on and :storefront_visible for the 5.5 cycle so out-of-tree integrations that still submit the legacy field on writes keep working. The accessor on the concern collapses both to the same underlying column. Clients should submit one or the other, not both — sending both is undefined behavior and depends on JSON key order in the request body. Document storefront_visible as the canonical name; treat display_on writes as a back-compat shim that disappears in 6.0.
// Before
export type DisplayOnValue = 'both' | 'front_end' | 'back_end'
export interface PaymentMethod {
display_on: DisplayOnValue
// …
}
// After (5.5) — boolean only; legacy types removed
export interface PaymentMethod {
storefront_visible: boolean
// …
}
DisplayOnValue / PaymentMethodDisplayOn types are removed entirely. The admin SDK is still next-tagged (per 5.5-admin-auth-cookie-refresh.md), so out-of-tree TypeScript consumers are minimal — deleting beats deprecating.
Param shapes (PaymentMethodCreateParams, PaymentMethodUpdateParams, future shipping equivalents) expose storefront_visible?: boolean only. The server still accepts display_on on writes (see Permitted params above), but the SDK doesn't type it. TS callers who need to send the legacy field can cast.
<StorefrontVisibleSwitch> — a labelled Switch ("Visible on storefront" / "Admin only" hint text). Used on every payment/shipping form.<ActiveBadge active={pm.storefront_visible} activeLabel="Visible" inactiveLabel="Admin only" /> so the visibility column matches the visual treatment of the existing Active column.The switch wires up like the rest: <StorefrontVisibleSwitch control={form.control} name="storefront_visible" />. No tri-state <DisplayOnSelect> exists — it was deleted in the same change that introduced the switch.
Part of the 6.0 model rename wave (alongside Shipment → Fulfillment, ShippingMethod → DeliveryMethod, Metafield → CustomField).
class AddStorefrontVisibleToPaymentMethods < ActiveRecord::Migration[7.2]
def change
add_column :spree_payment_methods, :storefront_visible, :boolean, default: true, null: false
# backfill via rake task before deploying the model change:
# UPDATE spree_payment_methods SET storefront_visible = (display_on != 'back_end')
remove_column :spree_payment_methods, :display_on, :string
end
end
Same shape for spree_shipping_methods (or spree_delivery_methods if the 6.0 table rename ships first — coordinate with 6.0-fulfillment-and-delivery.md).
Data migration in a rake task (not the migration itself, per CLAUDE.md: "Data transformations go in rake tasks, never in migrations"):
namespace :spree do
namespace :upgrade do
task display_on_to_storefront_visible: :environment do
Spree::PaymentMethod.unscoped.where(display_on: 'back_end').update_all(storefront_visible: false)
Spree::ShippingMethod.unscoped.where(display_on: 'back_end').update_all(storefront_visible: false)
# everything else (both, front_end, NULL) keeps the default true
end
end
end
include Spree::DisplayOn from PaymentMethod and ShippingMethod / DeliveryMethod.storefront_visible getter/setter shims (they become the actual column accessors).display_on API alias from serializers.Spree::DisplayOn concern file entirely (no more callers).DisplayOnValue type alias.display_on from PaymentMethod, ShippingMethod/DeliveryMethod interfaces.display_on? from all create/update param shapes.<DisplayOnSelect> component and DISPLAY_ON_LABELS constant in the admin SPA.display_on keeps working; storefront_visible becomes available.bin/rails spree:upgrade:display_on_to_storefront_visible against the production database before deploying the 6.0 code.storefront_visible (boolean) instead of display_on (string). Both ship side-by-side.storefront_visible in create/update calls. The server prefers it over display_on when both are provided.display_on disappears from responses and param shapes. Drop any code that reads or writes it.payment_method.display_on directly, switch to payment_method.storefront_visible.where(display_on: 'back_end'), use the new admin_only scope or where(storefront_visible: false).display_on references stop compiling — the column is gone.storefront_visible in serializers, controllers, models, specs, and the Admin SPA. display_on is in soft-deprecation as of this plan's adoption.<DisplayOnSelect> component shipped in [PR adding payment-method UX] stays callable for one minor cycle, but new forms should use the new <StorefrontVisibleSwitch> once it lands. The existing component is not deleted in 5.5 — it just has no new callers.MetafieldDefinition) — handled by 5.5-6.0-custom-fields-rename.md. Don't try to consolidate the two plans; the custom-fields one is further along and renames more than just the visibility field.Spree::DisplayOn concern is frozen. No new models should include Spree::DisplayOn; new resources that need storefront/admin visibility should add a storefront_visible:boolean column directly.available_on_front_end (the instance predicate from Spree::DisplayOn) in 5.5? Leaning yes — alias it to storefront_visible and add a deprecation warning when called. Removed in 6.0.display_on on ShippingMethod? Need to audit — if yes, the bridge needs to handle the Store API surface too, not just Admin.Spree::StorefrontVisible concern, or just keep inline columns + scopes per model? Defer until a third caller appears.5.5-6.0-custom-fields-rename.md — same bridge pattern, further along; the canonical reference for how to do this kind of rename.6.0-fulfillment-and-delivery.md — ShippingMethod → DeliveryMethod rename context. This plan touches the same model; coordinate the 6.0 migration order so the column rename and the table rename happen in the right order.decisions.md — visibility-system simplification decisions live here once this plan is finalized.