Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand All @@ -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

Expand Down Expand Up @@ -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
<a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
<div style="position: fixed; top: 0; right: 0; width: 180px; height: 180px; overflow: hidden; z-index: 9999; pointer-events: none;">
<div style="position: absolute; top: 50px; right: -40px; transform: rotate(45deg); background-color: rgba(220, 53, 69, 0.85); color: white; padding: 7px 40px; text-align: center; font-weight: bold; font-family: sans-serif; font-size: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); pointer-events: auto;">
React On Rails Pro Required
</div>
</div>
</a>
HTML
badge_html.strip.html_safe
end

def run_stream_inside_fiber
unless ReactOnRails::Utils.react_on_rails_pro?
Expand Down
17 changes: 16 additions & 1 deletion lib/react_on_rails/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment on lines +216 to +229
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Memoization bug: early returns bypass the cache assignment.

return inside the begin block exits the method before assigning @react_on_rails_pro_licence_valid, so calls where Pro isn’t installed (or legacy without licence_valid?) won’t be cached. Refactor to compute and assign without early returns.

Apply:

-    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.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 =
+        if 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.
+          if ReactOnRailsPro::Utils.respond_to?(:licence_valid?)
+            ReactOnRailsPro::Utils.licence_valid?
+          else
+            true
+          end
+        else
+          false
+        end
+    end

Optionally add a test-only reset helper to avoid cross‑example leakage:

def self.reset_react_on_rails_pro_licence_cache!
  remove_instance_variable(:@react_on_rails_pro_licence_valid) if instance_variable_defined?(:@react_on_rails_pro_licence_valid)
end
🤖 Prompt for AI Agents
In lib/react_on_rails/utils.rb around lines 216 to 229, the method currently
returns early inside the begin block which prevents assigning the computed value
to @react_on_rails_pro_licence_valid; refactor so you compute a local result (no
early returns), assign it to @react_on_rails_pro_licence_valid, and then return
it — specifically: check react_on_rails_pro? and whether ReactOnRailsPro::Utils
responds_to?(:licence_valid?) using conditional expressions that set a local
variable (true/false or the call to licence_valid?), assign that variable to the
instance variable, and return it; optionally add the suggested
reset_react_on_rails_pro_licence_cache! helper to clear the cache in tests by
removing the instance variable if defined.


def self.rsc_support_enabled?
return false unless react_on_rails_pro?

Expand Down
178 changes: 177 additions & 1 deletion spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" => "<div>Test</div>", "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" }
Expand All @@ -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
Expand Down
Loading