DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com on

Permission UI the Rails way

I’ve built many a SaaS. And most have pricing tiers: the higher the plans the more widgets you can create or features are allowed. Typically there are two ways to approach this:

  • hide the feature if the customer does not have access;
  • show all features, to all customers, but guard them with some, optional, upsell.

I have succesfully implementated the latter approach with a helper that quacks like Rails itself.

The helper lets you wrap content and decide what happens when access is denied. Hide it, redirect to a link, render content or render a partial. It’s small but flexible. This is how it looks (added a toggle allowed/denied states):

As always view on GitHub for a ready-to-copy code.

The API is elegant. Let’s look at some examples:

<%= allowed? Current.user.premium? do %>
  <%= link_to "Create project", projects_path %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

When Current.user.premium? is true, the link renders. When false, nothing shows. That’s the default behavior.

But you can do more. Redirect to a link instead:

<%= allowed? Current.user.premium?, redirect_to: upgrade_path do %>
  <%= link_to "Advanced analytics", analytics_path %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

The content becomes a clickable link pointing to your upgrade page.

Show plain text as a fallback:

<%= allowed? Current.user.premium?, render: { plain: "Upgrade to unlock this." } do %>
  <%= button_to "Delete project", project_path, method: :delete %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

Or render HTML:

<%= allowed? Current.user.premium?, render: { html: "Upgrade to the <strong>Pro plan</strong>." } do %>
  <%= button_to "Delete project", project_path, method: :delete %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

And render a partial for complex fallbacks:

<%= allowed? Current.user.premium?, render: "upsell" do %>
  <%= button_to "Advanced settings", settings_path %>
<% end %>

Enter fullscreen mode Exit fullscreen mode

The helper

Here’s the full helper in app/helpers/allowed_helper.rb:

module AllowedHelper
  def allowed?(condition, options = {})
    return yield if condition

    if options[:redirect_to]
      content = capture { yield }

      link_to strip_tags(content), options[:redirect_to]
    elsif options.key?(:render)
      render_options = options[:render]

      if render_options.is_a?(Hash) && render_options.key?(:html)
        render_options[:html].html_safe
      elsif render_options.is_a?(Hash) && render_options.key?(:plain)
        render_options[:plain]
      else
        render({ partial: render_options.to_s }) if render_options.is_a?(String) || render_options.is_a?(Symbol)
      end
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

The logic is straightforward. If the condition is true, render the block. If false, check your options and handle it accordingly.

When you pass redirect_to, it captures the block content, strips HTML tags and wraps it in a link. When you pass render, it supports three formats: plain text, raw HTML or a partial name as a string or symbol.

This helper did, ~10 years ago, not start like this. Over the years, after working on many dozens Rails-based SaaS apps, my aim became to make helpers “quack” more like Rails methods. You’re familiar with render plain:, render html: and partial rendering. This helper just extends that familiar syntax to conditional rendering.

I like it. What do you think of this?

Top comments (0)