From fb32341dbec9c4f2e95c9544a0a04704a0a9a568 Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 15 Apr 2026 18:48:11 +0200 Subject: [PATCH 1/8] feat: Add a Timezone module By including this module into a controller it will run all controller actions within the defined timezone. The timezone is taken from the params, the session, user preference (if existing), or the default timezone of the app. The resolved timezone is then stored in the session for future reference. --- .../spree/core/controller_helpers/timezone.rb | 55 ++++++++++ .../controller_helpers/timezone_spec.rb | 101 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 core/app/helpers/spree/core/controller_helpers/timezone.rb create mode 100644 core/spec/helpers/controller_helpers/timezone_spec.rb diff --git a/core/app/helpers/spree/core/controller_helpers/timezone.rb b/core/app/helpers/spree/core/controller_helpers/timezone.rb new file mode 100644 index 0000000000..6ed6cb00b9 --- /dev/null +++ b/core/app/helpers/spree/core/controller_helpers/timezone.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Spree + module Core + module ControllerHelpers + module Timezone + extend ActiveSupport::Concern + + included do + around_action :set_timezone + end + + private + + # Sets the timezone for the current request. + # + # Uses the most preferred timezone or falls back to the server default. + # + # It respects the server's configured timezone from +config/application.rb+. + # + def set_timezone(&action) + timezone = if timezone_change_needed? + resolved_timezone || Time.zone.name + else + session[:solidus_timezone] + end + session[:solidus_timezone] = timezone + Time.use_zone(timezone, &action) + end + + # Checks if we need to change the timezone or not. + def timezone_change_needed? + params[:solidus_timezone].present? || session[:solidus_timezone].blank? + end + + # Returns the first valid timezone from the priority chain, or nil. + # + # The priority order is: + # + # * the passed parameter: +params[:solidus_timezone]+ + # * the user's timezone preference + # + def resolved_timezone + candidates = [params[:solidus_timezone], timezone_from_user].compact + candidates.detect { |tz| ActiveSupport::TimeZone[tz].present? } + end + + # Try to get the timezone from user settings. + def timezone_from_user + spree_current_user.try(:timezone).presence + end + end + end + end +end diff --git a/core/spec/helpers/controller_helpers/timezone_spec.rb b/core/spec/helpers/controller_helpers/timezone_spec.rb new file mode 100644 index 0000000000..70fa22dbc0 --- /dev/null +++ b/core/spec/helpers/controller_helpers/timezone_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Spree::Core::ControllerHelpers::Timezone, type: :controller do + controller(ActionController::Base) do + include Spree::Core::ControllerHelpers::Timezone + + def index + render plain: Time.zone.name + end + + private + + attr_reader :spree_current_user + end + + let(:original_timezone) { Time.zone.name } + + describe "#set_timezone" do + context "with params[:solidus_timezone]" do + it "sets the timezone from the param" do + get :index, params: {solidus_timezone: "Hawaii"} + expect(response.body).to eq("Hawaii") + end + + it "stores the timezone in the session" do + get :index, params: {solidus_timezone: "Hawaii"} + expect(session[:solidus_timezone]).to eq("Hawaii") + end + + it "takes priority over session" do + get :index, params: {solidus_timezone: "Hawaii"}, session: {solidus_timezone: "Tokyo"} + expect(response.body).to eq("Hawaii") + end + end + + context "with session[:solidus_timezone]" do + it "uses the timezone from the session" do + get :index, session: {solidus_timezone: "Tokyo"} + expect(response.body).to eq("Tokyo") + end + end + + context "with spree_current_user timezone" do + let(:user) { double("User", timezone: "Berlin") } + + before do + controller.instance_variable_set(:@spree_current_user, user) + end + + it "uses the user's timezone" do + get :index + expect(response.body).to eq("Berlin") + end + + context "when user does not respond to timezone" do + let(:user) { double("User") } + + it "falls back to the server default" do + get :index + expect(response.body).to eq(original_timezone) + end + end + + context "when user's timezone is blank" do + let(:user) { double("User", timezone: "") } + + it "falls back to the server default" do + get :index + expect(response.body).to eq(original_timezone) + end + end + end + + context "with an invalid timezone" do + it "falls back to the server default" do + get :index, params: {solidus_timezone: "Nonexistent/Zone"} + expect(response.body).to eq(original_timezone) + end + end + + context "with no timezone set anywhere" do + it "uses the server default timezone" do + get :index + expect(response.body).to eq(original_timezone) + end + + it "stores the server default in session" do + get :index + expect(session[:solidus_timezone]).to eq(original_timezone) + end + end + + it "restores the original timezone after the request" do + original = Time.zone.name + get :index, params: {solidus_timezone: "Hawaii"} + expect(Time.zone.name).to eq(original) + end + end +end From 9df60d05912c00440793e6b7f5fa67ae9641de92 Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 15 Apr 2026 18:52:47 +0200 Subject: [PATCH 2/8] feat(backend): Handle timezone changes Use the `Spree::Core::ControllerHelpers::Timezone` module to wrap all admin actions inside the selected timezone. --- .../controllers/spree/admin/base_controller.rb | 2 ++ .../spree/admin/base_controller_spec.rb | 15 +++++++++++++++ .../spree/admin/stock_items_controller_spec.rb | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/app/controllers/spree/admin/base_controller.rb b/backend/app/controllers/spree/admin/base_controller.rb index 8d634c4922..38f5771001 100644 --- a/backend/app/controllers/spree/admin/base_controller.rb +++ b/backend/app/controllers/spree/admin/base_controller.rb @@ -3,6 +3,8 @@ module Spree module Admin class BaseController < Spree::BaseController + include Spree::Core::ControllerHelpers::Timezone + helper "spree/admin/navigation" layout "spree/layouts/admin" diff --git a/backend/spec/controllers/spree/admin/base_controller_spec.rb b/backend/spec/controllers/spree/admin/base_controller_spec.rb index ee106119ca..8824ccfc3c 100644 --- a/backend/spec/controllers/spree/admin/base_controller_spec.rb +++ b/backend/spec/controllers/spree/admin/base_controller_spec.rb @@ -38,4 +38,19 @@ def index end end end + + context "authorized request" do + stub_authorization! + + it "allows access" do + get :index + expect(response.body).to eq("test") + end + + it "sets timezone by param" do + get :index, params: {solidus_timezone: "Hawaii"} + expect(session).to have_key(:solidus_timezone) + expect(session[:solidus_timezone]).to eq("Hawaii") + end + end end diff --git a/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb b/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb index f0336bf573..774f90a8c5 100644 --- a/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb +++ b/backend/spec/controllers/spree/admin/stock_items_controller_spec.rb @@ -13,7 +13,7 @@ module Admin let(:stock_item) { variant.stock_items.first } let!(:user) { create :user } - before { expect(controller).to receive(:spree_current_user).and_return(user) } + before { expect(controller).to receive(:spree_current_user).twice.and_return(user) } before { request.env["HTTP_REFERER"] = "product_admin_page" } subject do From 8127cbfa69da9cdf56ee68443b70d1ea18a06988 Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 15 Apr 2026 18:58:20 +0200 Subject: [PATCH 3/8] feat(admin): Handle timezone changes Use the `Spree::Core::ControllerHelpers::Timezone` module to wrap all admin actions inside the selected timezone. Signed-off-by: Thomas von Deyen --- admin/app/controllers/solidus_admin/base_controller.rb | 1 + .../controllers/solidus_admin/base_controller_spec.rb | 8 +++++++- admin/spec/features/users_spec.rb | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/admin/app/controllers/solidus_admin/base_controller.rb b/admin/app/controllers/solidus_admin/base_controller.rb index 0cbab846af..7129735d1b 100644 --- a/admin/app/controllers/solidus_admin/base_controller.rb +++ b/admin/app/controllers/solidus_admin/base_controller.rb @@ -14,6 +14,7 @@ class BaseController < ApplicationController include SolidusAdmin::ControllerHelpers::Theme include SolidusAdmin::ComponentsHelper include SolidusAdmin::AuthenticationAdapters::Backend if defined?(Spree::Backend) + include Spree::Core::ControllerHelpers::Timezone layout :set_layout diff --git a/admin/spec/controllers/solidus_admin/base_controller_spec.rb b/admin/spec/controllers/solidus_admin/base_controller_spec.rb index fd4912092f..ed270a25f0 100644 --- a/admin/spec/controllers/solidus_admin/base_controller_spec.rb +++ b/admin/spec/controllers/solidus_admin/base_controller_spec.rb @@ -33,7 +33,7 @@ def index end end - context "successful request" do + context "authorized request" do before do user = create(:admin_user, email: "admin@example.com") allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(user) @@ -43,6 +43,12 @@ def index get :index expect(response.code).to eq "200" end + + it "sets timezone by param" do + get :index, params: {solidus_timezone: "Hawaii"} + expect(session).to have_key(:solidus_timezone) + expect(session[:solidus_timezone]).to eq("Hawaii") + end end describe "layout rendering" do diff --git a/admin/spec/features/users_spec.rb b/admin/spec/features/users_spec.rb index 7b5e2dc6da..86305e1d43 100644 --- a/admin/spec/features/users_spec.rb +++ b/admin/spec/features/users_spec.rb @@ -45,6 +45,7 @@ let(:sign_in_date) { DateTime.now } before do + allow_any_instance_of(Spree.user_class).to receive(:try).with(:timezone) { nil } allow_any_instance_of(Spree.user_class).to receive(:try).with(:email).and_call_original allow_any_instance_of(Spree.user_class).to receive(:try).with(:last_sign_in_at).and_return(sign_in_date) end From ca72a9fa14aebbd00b2e992bbff2be87fd52b2eb Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 15 Apr 2026 22:22:41 +0200 Subject: [PATCH 4/8] fix(admin navigation): Remove invalid html closing tag Spans do not self close and this actually has a closing tag anyway. Signed-off-by: Thomas von Deyen --- .../views/spree/admin/shared/_navigation_solidus_admin.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb b/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb index f73c798e4f..bcc6cc50b2 100644 --- a/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb +++ b/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb @@ -33,7 +33,7 @@ data-legacy-label="<%= 'spree.navigation.switch_to_legacy'.then { t(_1, default: t(_1, locale: :en)) } %>" data-admin-label="<%= 'spree.navigation.switch_to_solidus_admin'.then { t(_1, default: t(_1, locale: :en)) } %>" > - + From 6de4f3c56b547f9b97e02cd000ce46a130393e9b Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 15 Apr 2026 22:23:10 +0200 Subject: [PATCH 5/8] feat(admin): Add timezone select Signed-off-by: Thomas von Deyen --- .../layout/navigation/account/component.html.erb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/admin/app/components/solidus_admin/layout/navigation/account/component.html.erb b/admin/app/components/solidus_admin/layout/navigation/account/component.html.erb index 2a73d7aa67..d114afe080 100644 --- a/admin/app/components/solidus_admin/layout/navigation/account/component.html.erb +++ b/admin/app/components/solidus_admin/layout/navigation/account/component.html.erb @@ -27,6 +27,15 @@ shadow-base "> + <% available_timezones_for_select = ActiveSupport::TimeZone.all.map(&:name) %> +
  • + <%= autosubmit_select_tag( + "solidus_timezone", + options_for_select(available_timezones_for_select, selected: Time.zone.name), + icon: 'time-zone-line', + ) %> +
  • + <% if (available_locales = Spree.i18n_available_locales).any? %>
  • <%= autosubmit_select_tag( From 5d91dece61b86dd620ea25d3781aa0cd88c77124 Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 15 Apr 2026 22:23:24 +0200 Subject: [PATCH 6/8] feat(backend): Add timezone select Signed-off-by: Thomas von Deyen --- .../views/spree/admin/shared/_navigation.html.erb | 1 + .../admin/shared/_navigation_solidus_admin.html.erb | 1 + .../spree/admin/shared/_timezone_selection.html.erb | 10 ++++++++++ .../_timezone_selection_solidus_admin.html.erb | 13 +++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 backend/app/views/spree/admin/shared/_timezone_selection.html.erb create mode 100644 backend/app/views/spree/admin/shared/_timezone_selection_solidus_admin.html.erb diff --git a/backend/app/views/spree/admin/shared/_navigation.html.erb b/backend/app/views/spree/admin/shared/_navigation.html.erb index 36bb36e72e..23aa8eea7d 100644 --- a/backend/app/views/spree/admin/shared/_navigation.html.erb +++ b/backend/app/views/spree/admin/shared/_navigation.html.erb @@ -6,6 +6,7 @@ <%= button_tag class: 'btn fa fa-chevron-circle-left', id: 'admin-nav-toggle', type: :button do %> <%= t('spree.minimize_menu') %> <% end %> + <%= render partial: 'spree/admin/shared/timezone_selection' %> <%= render partial: 'spree/admin/shared/locale_selection' %> <%= render partial: 'spree/admin/shared/theme_selection' %> <% if lookup_context.exists?('spree/admin/shared/_navigation_footer') %> diff --git a/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb b/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb index bcc6cc50b2..a58e3d09ee 100644 --- a/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb +++ b/backend/app/views/spree/admin/shared/_navigation_solidus_admin.html.erb @@ -49,6 +49,7 @@
      + <%= render 'spree/admin/shared/timezone_selection_solidus_admin' %> <%= render 'spree/admin/shared/locale_selection_solidus_admin' %> <%= render 'spree/admin/shared/theme_selection_solidus_admin' %> diff --git a/backend/app/views/spree/admin/shared/_timezone_selection.html.erb b/backend/app/views/spree/admin/shared/_timezone_selection.html.erb new file mode 100644 index 0000000000..d5c6d14280 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_timezone_selection.html.erb @@ -0,0 +1,10 @@ +<% available_timezones_for_select = ActiveSupport::TimeZone.all.map(&:name) %> + +<%= form_tag(url_for, method: :get, style: "width: 100%;") do %> + +<% end %> diff --git a/backend/app/views/spree/admin/shared/_timezone_selection_solidus_admin.html.erb b/backend/app/views/spree/admin/shared/_timezone_selection_solidus_admin.html.erb new file mode 100644 index 0000000000..c9218d9f19 --- /dev/null +++ b/backend/app/views/spree/admin/shared/_timezone_selection_solidus_admin.html.erb @@ -0,0 +1,13 @@ +<% available_timezones_for_select = ActiveSupport::TimeZone.all.map(&:name) %> + +
    • + <%= form_tag(url_for, method: :get, style: "width: 100%;") do %> + + <% end %> +
    • From 45c29e49030fa7733ee43762c0efd6975932b2cf Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 22 Apr 2026 21:46:09 +0200 Subject: [PATCH 7/8] feat(backend): Add timezone select to users form If the user has a timezone column we show a select to let admins set the preferred time zone of the user. --- backend/app/views/spree/admin/users/_form.html.erb | 9 +++++++++ core/lib/spree/permitted_attributes.rb | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/app/views/spree/admin/users/_form.html.erb b/backend/app/views/spree/admin/users/_form.html.erb index 760d62a516..d95d5b7041 100644 --- a/backend/app/views/spree/admin/users/_form.html.erb +++ b/backend/app/views/spree/admin/users/_form.html.erb @@ -76,5 +76,14 @@ <% end %> <% end %> <% end %> + + <% if can?(:update, @user) && @user.respond_to?(:timezone) %> + <%= f.field_container :timezone do %> + <%= f.label :timezone %> + <%= f.collection_select :timezone, ActiveSupport::TimeZone.all, :name, :to_s, + {include_blank: t("spree.none")}, + class: "select2 fullwidth" %> + <% end %> + <% end %> diff --git a/core/lib/spree/permitted_attributes.rb b/core/lib/spree/permitted_attributes.rb index 7a3f9c54ae..0e8097064c 100644 --- a/core/lib/spree/permitted_attributes.rb +++ b/core/lib/spree/permitted_attributes.rb @@ -138,7 +138,7 @@ module PermittedAttributes # by changing a user with higher priveleges' email to one a lower-priveleged # admin owns. Creating a user with an email is handled separate at the # controller level. - @@user_attributes = [:password, :password_confirmation, customer_metadata: {}] + @@user_attributes = [:password, :password_confirmation, :timezone, customer_metadata: {}] @@variant_attributes = [ :name, :presentation, :cost_price, :lock_version, From def12cd69e8412a4a880ffdfacd9a4efe18938ef Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Wed, 22 Apr 2026 22:33:44 +0200 Subject: [PATCH 8/8] feat(admin): Add timezone select to users form If the user has a timezone column we show a select to let admins set the preferred time zone of the user. --- .../components/solidus_admin/users/edit/component.html.erb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/admin/app/components/solidus_admin/users/edit/component.html.erb b/admin/app/components/solidus_admin/users/edit/component.html.erb index 98401c5d7d..ca84d74d39 100644 --- a/admin/app/components/solidus_admin/users/edit/component.html.erb +++ b/admin/app/components/solidus_admin/users/edit/component.html.erb @@ -29,6 +29,11 @@
      <%= f.text_field(:password_confirmation) %>
      + <% if @user.respond_to?(:timezone) %> +
      + <%= f.select :timezone, ActiveSupport::TimeZone.all.map { |t| [t.name, t.to_s] }, include_blank: t("spree.none") %> +
      + <% end %>
      <%= f.checkbox_row(:spree_role_ids, options: role_options, row_title: "Roles", layout: :subsection) %>