diff --git a/app/views/decidim/devise/shared/omniauth_buttons/_default.html.erb b/app/views/decidim/devise/shared/omniauth_buttons/_default.html.erb index a5913ad1de..1640ab4cb5 100644 --- a/app/views/decidim/devise/shared/omniauth_buttons/_default.html.erb +++ b/app/views/decidim/devise/shared/omniauth_buttons/_default.html.erb @@ -1,10 +1,15 @@ -<% link_classes = "login__omniauth-button button--#{normalize_provider_name(provider)}" %> -<% display_name = current_organization.enabled_omniauth_providers.dig(provider.to_sym, :display_name) %> -<% provider_name = display_name.present? ? display_name : normalize_provider_name(provider).titleize %> -<% provider_name = I18n.t("decidim.devise.shared.links.log_in_with_provider") if custom_publik_translation?(provider) %> -<%= link_to decidim.send("user_#{provider}_omniauth_authorize_path"), class: link_classes, method: :post, title: t("devise.shared.links.log_in_with_provider", provider: provider_name) do %> - <%= oauth_icon provider %> - - <%= provider_name %> - +<% provider_name = omniauth_provider_label(provider) %> +<% if full_image_button?(provider) %> + <% link_classes = "login__omniauth-image-button image-button--#{normalize_provider_name(provider)} group/oauth-icon" %> + <%= link_to decidim.send("user_#{provider}_omniauth_authorize_path"), class: link_classes, method: :post, title: t("devise.shared.links.log_in_with_provider", provider: provider_name) do %> + <%= oauth_icon_with_hover provider %> + <% end %> +<% else %> + <% link_classes = "login__omniauth-button button--#{normalize_provider_name(provider)}" %> + <%= link_to decidim.send("user_#{provider}_omniauth_authorize_path"), class: link_classes, method: :post, title: t("devise.shared.links.log_in_with_provider", provider: provider_name) do %> + <%= oauth_icon provider %> + + <%= provider_name %> + + <% end %> <% end %> diff --git a/app/views/decidim/devise/shared/omniauth_buttons/_france_connect.html.erb b/app/views/decidim/devise/shared/omniauth_buttons/_france_connect.html.erb index 1e4efa9968..423f932faa 100644 --- a/app/views/decidim/devise/shared/omniauth_buttons/_france_connect.html.erb +++ b/app/views/decidim/devise/shared/omniauth_buttons/_france_connect.html.erb @@ -1,13 +1,8 @@ -<% link_classes = "login__omniauth-image-button image-button--#{normalize_provider_name(provider)} group/oauth-icon" %> -<% display_name = current_organization.enabled_omniauth_providers.dig(provider.to_sym, :display_name) %> -<% provider_name = display_name.present? ? display_name : normalize_provider_name(provider).titleize %>

<%= t("decidim.devise.omniauth.france_connect.introduction") %>

- <%= link_to decidim.send("user_#{provider}_omniauth_authorize_path"), class: link_classes, method: :post, title: t("devise.shared.links.log_in_with_provider", provider: provider_name) do %> - <%= oauth_icon_with_hover provider %> - <% end %> + <%= render partial: "decidim/devise/shared/omniauth_buttons/default", locals: { provider: provider } %>

<%= link_to "https://franceconnect.gouv.fr/", class: "text-secondary hover:underline", target: "_blank", title: t("decidim.devise.omniauth.france_connect.help_link") do %> <%= t("decidim.devise.omniauth.france_connect.help_link") %> diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 4bf887805b..88aedfad77 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -151,3 +151,4 @@ ignore_unused: - decidim.searches.filters.jump_to - decidim.components.proposals.settings.global.enable_iframe - decidim.components.proposals.settings.global.iframe_url + - decidim.devise.shared.links.(log_in_with_provider|provider_display_name) diff --git a/config/locales/en.yml b/config/locales/en.yml index 77af8e4899..9568042ae4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -117,6 +117,7 @@ en: shared: links: log_in_with_provider: Log in with %{provider} + provider_display_name: "%{provider}" events: proposals: author_confirmation_proposal_event: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f7af0918a2..13264ce71f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -125,6 +125,7 @@ fr: shared: links: log_in_with_provider: Connectez-vous avec %{provider} + provider_display_name: "%{provider}" events: proposals: author_confirmation_proposal_event: diff --git a/lib/extends/helpers/decidim/omniauth_helper_extends.rb b/lib/extends/helpers/decidim/omniauth_helper_extends.rb index 89f9c21fe9..1aa9bdd015 100644 --- a/lib/extends/helpers/decidim/omniauth_helper_extends.rb +++ b/lib/extends/helpers/decidim/omniauth_helper_extends.rb @@ -1,36 +1,101 @@ # frozen_string_literal: true module OmniauthHelperExtends - # Public: icon for omniauth buttons - def oauth_icon_with_hover(provider) - info = current_organization.enabled_omniauth_providers[provider.to_sym] + FRANCE_CONNECT_DEFAULT_ICONS = { + icon_path: "media/images/franceconnect-btn-principal.svg", + icon_hover_path: "media/images/franceconnect-btn-principal-hover.svg" + }.freeze - icon_path = info&.dig(:icon_path) - icon_hover_path = info&.dig(:icon_hover_path) + FALLBACK_ICON_NAME = "login-box-line" - if provider.to_sym == :france_connect - icon_path = "media/images/franceconnect-btn-principal.svg" if icon_path.blank? - icon_hover_path = "media/images/franceconnect-btn-principal-hover.svg" if icon_hover_path.blank? - end + def full_image_button?(provider) + icons = provider_icon_settings(provider) + icons[:icon_path].present? && icons[:icon_hover_path].present? + end - return oauth_icon(provider) unless icon_path.present? && icon_hover_path.present? + def oauth_icon_with_hover(provider) + icons = provider_icon_settings(provider) + return oauth_icon(provider) unless icons[:icon_path].present? && icons[:icon_hover_path].present? - # parent html element needs to have the "group/oauth-icon" tailwind class - icon_html = external_icon(icon_path, class: "block group-hover/oauth-icon:hidden") - icon_html += external_icon(icon_hover_path, class: "hidden group-hover/oauth-icon:block") + icon_html = external_icon(icons[:icon_path], class: "block group-hover/oauth-icon:hidden") + icon_html += external_icon(icons[:icon_hover_path], class: "hidden group-hover/oauth-icon:block") icon_html.html_safe end + def oauth_icon(provider) + icons = provider_icon_settings(provider) + return external_icon(icons[:icon_path]) if icons[:icon_path].present? + + icon(safe_icon_name(provider, icons[:icon])) + end + + def omniauth_provider_label(provider) + custom_provider_display_name(provider) || + current_organization.enabled_omniauth_providers.dig(provider.to_sym, :display_name).presence || + humanized_provider_name(provider) + end + def normalize_provider_name(provider) return "x" if provider == :twitter - # customize the name of the omniauth btn login with publik - return I18n.t("decidim.devise.shared.links.log_in_with_provider") if custom_publik_translation?(provider) provider.to_s.split("_").first end - def custom_publik_translation?(provider) - provider == :publik && Decidim::TermCustomizer::Translation.where(key: "decidim.devise.shared.links.log_in_with_provider").present? + private + + def custom_provider_display_name(provider) + scoped_key = "decidim.devise.shared.links.provider_display_name_#{provider}" + return I18n.t(scoped_key, provider: humanized_provider_name(provider)) if term_customizer_override?(scoped_key) + + shared_key = "decidim.devise.shared.links.provider_display_name" + return I18n.t(shared_key, provider: humanized_provider_name(provider)) if term_customizer_override?(shared_key) + + nil + end + + def term_customizer_override?(key) + defined?(Decidim::TermCustomizer::Translation) && + Decidim::TermCustomizer::Translation.exists?(key:) + end + + def humanized_provider_name(provider) + return "X" if provider.to_sym == :twitter + return "Google" if provider.to_sym == :google_oauth2 + + provider.to_s.tr("_", " ").titleize + end + + def provider_icon_settings(provider) + db_info = current_organization.enabled_omniauth_providers[provider.to_sym] || {} + global_info = global_omniauth_provider_config(provider) + + settings = { + icon_path: db_info[:icon_path].presence || global_info[:icon_path].presence, + icon_hover_path: db_info[:icon_hover_path].presence || global_info[:icon_hover_path].presence, + icon: db_info[:icon].presence || global_info[:icon].presence + } + + settings.merge!(FRANCE_CONNECT_DEFAULT_ICONS) if provider.to_sym == :france_connect && settings[:icon_path].blank? + settings + end + + def global_omniauth_provider_config(provider) + return {} unless Decidim.respond_to?(:omniauth_providers) + + Decidim.omniauth_providers[provider.to_sym] || {} + rescue StandardError + {} + end + + def safe_icon_name(provider, configured_icon) + candidates = [configured_icon, "#{normalize_provider_name(provider)}-fill", FALLBACK_ICON_NAME].compact + candidates.find { |name| icon_registered?(name) } || FALLBACK_ICON_NAME + end + + def icon_registered?(name) + Decidim.icons.all.has_key?(name.to_s) + rescue StandardError + false end end diff --git a/spec/helpers/decidim/omniauth_helper_spec.rb b/spec/helpers/decidim/omniauth_helper_spec.rb index 4ae18a8664..6f1809de9f 100644 --- a/spec/helpers/decidim/omniauth_helper_spec.rb +++ b/spec/helpers/decidim/omniauth_helper_spec.rb @@ -4,21 +4,16 @@ module Decidim describe OmniauthHelper do - let(:facebook_enabled) { true } - let(:twitter_enabled) { true } - let(:publik_enabled) { true } - let(:secrets) do - { - omniauth: { - facebook: { enabled: facebook_enabled }, - twitter: { enabled: twitter_enabled }, - publik: { enabled: publik_enabled } - } - } - end + before { Rails.cache.clear } + + shared_context "with a stubbed organization" do + let(:organization) { create(:organization, host: "test-#{SecureRandom.hex(4)}.org") } + let(:enabled_providers) { {} } - before do - allow(Rails.application).to receive(:secrets).and_return(secrets) + before do + allow(helper).to receive(:current_organization).and_return(organization) + allow(organization).to receive(:enabled_omniauth_providers).and_return(enabled_providers) + end end describe "#normalize_provider_name" do @@ -28,16 +23,143 @@ module Decidim end end - context "when provider is publik" do + context "when provider is twitter" do + it "returns x" do + expect(helper.normalize_provider_name(:twitter)).to eq("x") + end + end + + context "when provider is a composed name" do + it "truncates to the first segment (used for CSS classes only, never shown to users)" do + expect(helper.normalize_provider_name(:cultuur_connect)).to eq("cultuur") + expect(helper.normalize_provider_name(:france_connect)).to eq("france") + end + end + end + + describe "#omniauth_provider_label" do + include_context "with a stubbed organization" + + context "when a Term Customizer override exists in the database" do + let(:enabled_providers) { { publik: {} } } + let(:key) { "decidim.devise.shared.links.provider_display_name_publik" } let(:translation_set) { create(:translation_set) } - let!(:translation) { create(:translation, key: "decidim.devise.shared.links.log_in_with_provider", value: "Login with MyPublik") } + let!(:translation) { create(:translation, translation_set:, key:, value: "Login with MyPublik") } + + it "detects the override exists" do + expect(helper.send(:term_customizer_override?, key)).to be(true) + end + end + + context "when I18n resolves a Term Customizer override for the provider" do + let(:enabled_providers) { { publik: {} } } + let(:key) { "decidim.devise.shared.links.provider_display_name_publik" } + + before do + allow(helper).to receive(:term_customizer_override?).with(key).and_return(true) + allow(I18n).to receive(:t).with(key, provider: "Publik").and_return("Login with MyPublik") + end + + it "returns that translation, taking priority over display_name and the humanized fallback" do + expect(helper.omniauth_provider_label(:publik)).to eq("Login with MyPublik") + end + end + + context "when no Term Customizer translation exists but display_name is configured" do + let(:enabled_providers) { { cultuur_connect: { display_name: "Mon SSO" } } } + + it "returns the configured display_name" do + expect(helper.omniauth_provider_label(:cultuur_connect)).to eq("Mon SSO") + end + end + + context "when nothing is configured" do + let(:enabled_providers) { { cultuur_connect: {} } } + + it "returns the full humanized provider name, not truncated" do + expect(helper.omniauth_provider_label(:cultuur_connect)).to eq("Cultuur Connect") + end + end + + context "when provider is twitter with nothing configured" do + let(:enabled_providers) { { twitter: {} } } + + it "returns X" do + expect(helper.omniauth_provider_label(:twitter)).to eq("X") + end + end + + context "when provider is google_oauth2 with nothing configured" do + let(:enabled_providers) { { google_oauth2: {} } } + + it "returns Google, not Google Oauth2" do + expect(helper.omniauth_provider_label(:google_oauth2)).to eq("Google") + end + end + end + + describe "#full_image_button?" do + include_context "with a stubbed organization" + + context "when both icon_path and icon_hover_path are configured" do + let(:enabled_providers) { { openid_connect: { icon_path: "a.svg", icon_hover_path: "b.svg" } } } + + it "returns true" do + expect(helper.full_image_button?(:openid_connect)).to be(true) + end + end + + context "when only icon_path is configured" do + let(:enabled_providers) { { openid_connect: { icon_path: "a.svg" } } } + + it "returns false" do + expect(helper.full_image_button?(:openid_connect)).to be(false) + end + end + + context "when provider is france_connect with no icon configured at all" do + let(:enabled_providers) { { france_connect: {} } } + + it "returns true, using the hardcoded default icons" do + expect(helper.full_image_button?(:france_connect)).to be(true) + end + end + end + + describe "#oauth_icon" do + include_context "with a stubbed organization" + + context "when Decidim.omniauth_providers is not defined on this Decidim version" do + let(:enabled_providers) { { cultuur_connect: {} } } + + before do + allow(Decidim).to receive(:respond_to?).with(:omniauth_providers).and_return(false) + end + + it "does not raise and falls back to the generic icon" do + expect { helper.oauth_icon(:cultuur_connect) }.not_to raise_error + end + end + + context "when the provider has an icon_path configured" do + let(:enabled_providers) { { facebook: { icon_path: "facebook.svg" } } } + + it "renders the external icon" do + expect(helper).to receive(:external_icon).with("facebook.svg") + helper.oauth_icon(:facebook) + end + end + + context "when nothing is configured and the guessed icon name isn't registered" do + let(:enabled_providers) { { openid_connect: {} } } before do - allow(I18n).to receive(:t).with("decidim.devise.shared.links.log_in_with_provider").and_return("Login with MyPublik") + allow(Decidim.icons).to receive(:all).and_return({}) end - it "returns the specific translation key" do - expect(helper.normalize_provider_name(:publik)).to eq("Login with MyPublik") + it "falls back to the hardcoded fallback icon name" do + expect(helper).to receive(:icon).with(OmniauthHelperExtends::FALLBACK_ICON_NAME) + helper.oauth_icon(:openid_connect) end end end diff --git a/spec/system/registration_spec.rb b/spec/system/registration_spec.rb index 774fea6c12..c7b9509942 100644 --- a/spec/system/registration_spec.rb +++ b/spec/system/registration_spec.rb @@ -41,6 +41,13 @@ def fill_registration_form( end end end + + describe "omniauth buttons", :caching do + it "renders every configured provider without raising an error" do + expect(page).to have_no_content("We're sorry, but something went wrong") + expect(page).to have_css(".login__omniauth-button, .login__omniauth-image-button", minimum: 1) + end + end end context "when newsletter checkbox is unchecked" do