Back to Spree

`display_on` → `storefront_visible` (PaymentMethod, ShippingMethod / DeliveryMethod)

docs/plans/5.5-6.0-display-on-to-boolean.md

5.5.014.2 KB
Original Source

display_onstorefront_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

Summary

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:

  1. 5.5 — Add 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.
  2. 6.0 — Replace the display_on string column with storefront_visible boolean on both tables. Remove the Spree::DisplayOn concern. Drop the display_on API alias.

Key Decisions (do not deviate without discussion)

  • Boolean, not tri-state. 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.
  • Default true. Most payment/shipping methods are customer-facing. back_end is the exception (manual check entry, internal wire-transfer methods, refund-only methods).
  • 5.5 reads expose 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.
  • Drop 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.
  • Migration is non-destructive in 5.5. No data migration, no removed columns. Anyone reading display_on keeps working; anyone reading storefront_visible gets the boolean.
  • The front_end-only edge case (if any merchant set it) maps to storefront_visible: true. Same as the custom-fields plan's data migration.

Design Details

Current model (5.4)

Spree::PaymentMethod and Spree::ShippingMethod both:

  • Have a display_on string column (values: 'both' | 'front_end' | 'back_end').
  • include Spree::DisplayOn (concern at spree/core/app/models/concerns/spree/display_on.rb).
  • Expose 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 predicate

Target model (6.0)

  • display_on:string column → storefront_visible:boolean (default true, null: false).
  • Concern goes away. Scopes/predicates inline on each model (or a smaller Spree::StorefrontVisible concern if more models want it later — defer until a third caller exists).
  • Serializers expose storefront_visible:boolean. The display_on field is removed from the wire.

Phase 1: 5.5 API Bridge + alias_attribute shim

Mirrors 5.5-6.0-custom-fields-rename.md exactly. No schema changes.

Shared accessor + scopes inside Spree::DisplayOn

Spree::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:

ruby
# 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_endfalse, anything else (both or the legacy front_end) → true. Writes: trueboth, falseback_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).

Serializer changes

Admin::PaymentMethodSerializer (and any future Admin::ShippingMethodSerializer):

ruby
# 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.

Permitted params

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.

SDK / TypeScript

ts
// 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.

Admin SPA

  • <StorefrontVisibleSwitch> — a labelled Switch ("Visible on storefront" / "Admin only" hint text). Used on every payment/shipping form.
  • Table cells render via <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.

Phase 2: 6.0 Schema Change

Part of the 6.0 model rename wave (alongside Shipment → Fulfillment, ShippingMethod → DeliveryMethod, Metafield → CustomField).

Migrations

ruby
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"):

ruby
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

Model cleanup

  • Remove include Spree::DisplayOn from PaymentMethod and ShippingMethod / DeliveryMethod.
  • Remove the storefront_visible getter/setter shims (they become the actual column accessors).
  • Remove the display_on API alias from serializers.
  • Delete the Spree::DisplayOn concern file entirely (no more callers).

SDK cleanup

  • Remove DisplayOnValue type alias.
  • Remove display_on from PaymentMethod, ShippingMethod/DeliveryMethod interfaces.
  • Remove display_on? from all create/update param shapes.
  • Delete the <DisplayOnSelect> component and DISPLAY_ON_LABELS constant in the admin SPA.

Migration Path

For Spree merchants / hosters (5.5 → 6.0)

  1. Update Spree to 5.5 — no action required. display_on keeps working; storefront_visible becomes available.
  2. Update Spree to 6.0 — run bin/rails spree:upgrade:display_on_to_storefront_visible against the production database before deploying the 6.0 code.

For integrations consuming the Admin / Store API

  1. 5.5: start reading storefront_visible (boolean) instead of display_on (string). Both ship side-by-side.
  2. 5.5: start writing storefront_visible in create/update calls. The server prefers it over display_on when both are provided.
  3. 6.0: display_on disappears from responses and param shapes. Drop any code that reads or writes it.

For extension / plugin developers

  1. 5.5: if your extension reads payment_method.display_on directly, switch to payment_method.storefront_visible.
  2. 5.5: if your extension queries where(display_on: 'back_end'), use the new admin_only scope or where(storefront_visible: false).
  3. 6.0: any remaining display_on references stop compiling — the column is gone.

Constraints on Current Work

  • All new Spree code (5.5+) should use storefront_visible in serializers, controllers, models, specs, and the Admin SPA. display_on is in soft-deprecation as of this plan's adoption.
  • The <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.
  • Custom Fields (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.
  • The 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.

Open Questions

  • Should we also rename 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.
  • Does the Store API expose display_on on ShippingMethod? Need to audit — if yes, the bridge needs to handle the Store API surface too, not just Admin.
  • Naming for the 6.0 concern (if any): if a third caller wants storefront/admin visibility post-6.0, do we extract a Spree::StorefrontVisible concern, or just keep inline columns + scopes per model? Defer until a third caller appears.

References

  • 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.mdShippingMethod → 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.