diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 34837cc953..60fe4551e4 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -58,15 +58,17 @@ def react_component(component_name, options = {}) server_rendered_html = internal_result[:result]["html"] console_script = internal_result[:result]["consoleReplayScript"] render_options = internal_result[:render_options] + badge = pro_warning_badge_if_needed(render_options.force_load) case server_rendered_html when String - build_react_component_result_for_server_rendered_string( + html = build_react_component_result_for_server_rendered_string( server_rendered_html: server_rendered_html, component_specification_tag: internal_result[:tag], console_script: console_script, render_options: render_options ) + (badge + html).html_safe when Hash msg = <<~MSG Use react_component_hash (not react_component) to return a Hash to your ruby view code. See @@ -212,18 +214,21 @@ def react_component_hash(component_name, options = {}) server_rendered_html = internal_result[:result]["html"] console_script = internal_result[:result]["consoleReplayScript"] render_options = internal_result[:render_options] + badge = pro_warning_badge_if_needed(render_options.force_load) if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"] server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] } end if server_rendered_html.is_a?(Hash) - build_react_component_result_for_server_rendered_hash( + result = build_react_component_result_for_server_rendered_hash( server_rendered_html: server_rendered_html, component_specification_tag: internal_result[:tag], console_script: console_script, render_options: render_options ) + result[COMPONENT_HTML_KEY] = badge + result[COMPONENT_HTML_KEY] + result else msg = <<~MSG Render-Function used by react_component_hash for #{component_name} is expected to return @@ -251,6 +256,8 @@ def react_component_hash(component_name, options = {}) # waiting for the page to load. def redux_store(store_name, props: {}, defer: false, force_load: nil) force_load = ReactOnRails.configuration.force_load if force_load.nil? + badge = pro_warning_badge_if_needed(force_load) + redux_store_data = { store_name: store_name, props: props, force_load: force_load } @@ -261,7 +268,7 @@ def redux_store(store_name, props: {}, defer: false, force_load: nil) else registered_stores << redux_store_data result = render_redux_store_data(redux_store_data) - prepend_render_rails_context(result) + (badge + prepend_render_rails_context(result)).html_safe end end @@ -440,7 +447,28 @@ def load_pack_for_generated_component(react_component_name, render_options) # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity - private + def pro_warning_badge_if_needed(force_load) + return "".html_safe unless force_load + return "".html_safe if ReactOnRails::Utils.react_on_rails_pro_licence_valid? + + warning_message = "[REACT ON RAILS] The 'force_load' feature requires a React on Rails Pro license. " \ + "Please visit https://shakacode.com/react-on-rails-pro to learn more." + puts warning_message + Rails.logger.warn warning_message + + tooltip_text = "The 'force_load' feature requires a React on Rails Pro license. Click to learn more." + + badge_html = <<~HTML + +
+
+ React On Rails Pro Required +
+
+
+ HTML + badge_html.strip.html_safe + end def run_stream_inside_fiber unless ReactOnRails::Utils.react_on_rails_pro? diff --git a/lib/react_on_rails/utils.rb b/lib/react_on_rails/utils.rb index bca6b39a9f..3243ab473d 100644 --- a/lib/react_on_rails/utils.rb +++ b/lib/react_on_rails/utils.rb @@ -7,7 +7,7 @@ require "active_support/core_ext/string" module ReactOnRails - module Utils + module Utils # rubocop:disable Metrics/ModuleLength TRUNCATION_FILLER = "\n... TRUNCATED #{ Rainbow('To see the full output, set FULL_TEXT_ERRORS=true.').red } ...\n".freeze @@ -213,6 +213,21 @@ def self.react_on_rails_pro_version end end + def self.react_on_rails_pro_licence_valid? + return @react_on_rails_pro_licence_valid if defined?(@react_on_rails_pro_licence_valid) + + @react_on_rails_pro_licence_valid = begin + return false unless react_on_rails_pro? + + # Maintain compatibility with legacy versions of React on Rails Pro: + # Earlier releases did not require license validation, as they were distributed as private gems. + # This check ensures that the method works correctly regardless of the installed version. + return true unless ReactOnRailsPro::Utils.respond_to?(:licence_valid?) + + ReactOnRailsPro::Utils.licence_valid? + end + end + def self.rsc_support_enabled? return false unless react_on_rails_pro? diff --git a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb index b1e9f7d29b..17e2bfd981 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -21,6 +21,10 @@ class PlainReactOnRailsHelper { "HTTP_ACCEPT_LANGUAGE" => "en" } ) } + + allow(ReactOnRails::Utils).to receive_messages( + react_on_rails_pro_licence_valid?: true + ) end let(:hash) do @@ -370,10 +374,137 @@ def helper.append_javascript_pack_tag(name, **options) it { is_expected.to include force_load_script } end end + + describe "with Pro license warning" do + let(:badge_html_string) { "React On Rails Pro Required" } + + before do + allow(Rails.logger).to receive(:warn) + end + + context "when Pro license is NOT installed and force_load is true" do + subject(:react_app) { react_component("App", props: props, force_load: true) } + + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro_licence_valid?).and_return(false) + end + + it { is_expected.to include(badge_html_string) } + + it "logs a warning" do + react_app + expect(Rails.logger).to have_received(:warn).with(a_string_matching(/The 'force_load' feature requires/)) + end + end + + context "when Pro license is NOT installed and global force_load is true" do + subject(:react_app) { react_component("App", props: props) } + + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro_licence_valid?).and_return(false) + end + + around do |example| + ReactOnRails.configure { |config| config.force_load = true } + example.run + ReactOnRails.configure { |config| config.force_load = false } + end + + it { is_expected.to include(badge_html_string) } + end + + context "when Pro license is NOT installed and force_load is false" do + subject(:react_app) { react_component("App", props: props, force_load: false) } + + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro_licence_valid?).and_return(false) + end + + it { is_expected.not_to include(badge_html_string) } + + it "does not log a warning" do + react_app + expect(Rails.logger).not_to have_received(:warn) + end + end + + context "when Pro license IS installed and force_load is true" do + subject(:react_app) { react_component("App", props: props, force_load: true) } + + before do + allow(ReactOnRails::Utils).to receive_messages( + react_on_rails_pro_licence_valid?: true + ) + end + + it { is_expected.not_to include(badge_html_string) } + + it "does not log a warning" do + react_app + expect(Rails.logger).not_to have_received(:warn) + end + end + end + end + + describe "#react_component_hash" do + subject(:react_app) { react_component_hash("App", props: props) } + + let(:props) { { name: "My Test Name" } } + + before do + allow(SecureRandom).to receive(:uuid).and_return(0) + allow(ReactOnRails::ServerRenderingPool).to receive(:server_render_js_with_console_logging).and_return( + "html" => { "componentHtml" => "
Test
", "title" => "Test Title" }, + "consoleReplayScript" => "" + ) + allow(ReactOnRails::ServerRenderingJsCode).to receive(:js_code_renderer) + .and_return(ReactOnRails::ServerRenderingJsCode) + end + + it "returns a hash with component and other keys" do + expect(react_app).to be_a(Hash) + expect(react_app).to have_key("componentHtml") + expect(react_app).to have_key("title") + end + + context "with Pro license warning" do + let(:badge_html_string) { "React On Rails Pro Required" } + + before do + allow(Rails.logger).to receive(:warn) + end + + context "when Pro license is NOT installed and force_load is true" do + subject(:react_app) { react_component_hash("App", props: props, force_load: true) } + + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro_licence_valid?).and_return(false) + end + + it "adds badge to componentHtml" do + expect(react_app["componentHtml"]).to include(badge_html_string) + end + end + + context "when Pro license IS installed and force_load is true" do + subject(:react_app) { react_component_hash("App", props: props, force_load: true) } + + before do + allow(ReactOnRails::Utils).to receive_messages( + react_on_rails_pro_licence_valid?: true + ) + end + + it "does not add badge to componentHtml" do + expect(react_app["componentHtml"]).not_to include(badge_html_string) + end + end + end end describe "#redux_store" do - subject(:store) { redux_store("reduxStore", props: props) } + subject(:store) { redux_store("reduxStore", props: props, force_load: true) } let(:props) do { name: "My Test Name" } @@ -394,6 +525,51 @@ def helper.append_javascript_pack_tag(name, **options) it { expect(expect(store).target).to script_tag_be_included(react_store_script) } + + context "with Pro license warning" do + let(:badge_html_string) { "React On Rails Pro Required" } + + before do + allow(Rails.logger).to receive(:warn) + end + + context "when Pro license is NOT installed and force_load is true" do + subject(:store) { redux_store("reduxStore", props: props, force_load: true) } + + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro_licence_valid?).and_return(false) + end + + it { is_expected.to include(badge_html_string) } + + it "logs a warning" do + store + expect(Rails.logger).to have_received(:warn).with(a_string_matching(/The 'force_load' feature requires/)) + end + end + + context "when Pro license is NOT installed and force_load is false" do + subject(:store) { redux_store("reduxStore", props: props, force_load: false) } + + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro_licence_valid?).and_return(false) + end + + it { is_expected.not_to include(badge_html_string) } + end + + context "when Pro license IS installed and force_load is true" do + subject(:store) { redux_store("reduxStore", props: props, force_load: true) } + + before do + allow(ReactOnRails::Utils).to receive_messages( + react_on_rails_pro_licence_valid?: true + ) + end + + it { is_expected.not_to include(badge_html_string) } + end + end end describe "#server_render_js", :js, type: :system do