Module: Sessions::EngineHelper

Defined in:
app/helpers/sessions/engine_helper.rb

Overview

View helpers for the devices page (and for the host-renderable partials).

Constant Summary collapse

DEVICE_ICONS =
{
  "desktop" => "🖥",
  "smartphone" => "📱",
  "tablet" => "📱",
  "native_ios" => "📱",
  "native_android" => "📱",
  "bot" => "🤖"
}.freeze
DEVICE_ICON_NAMES =

Semantic icon names (Heroicons vocabulary — map them onto whatever icon helper your app uses) so custom views don't re-derive the device_type → icon mapping the gem already knows.

{
  "desktop" => "computer-desktop",
  "smartphone" => "device-phone-mobile",
  "tablet" => "device-tablet",
  "native_ios" => "device-phone-mobile",
  "native_android" => "device-phone-mobile",
  "bot" => "bug-ant"
}.freeze
EVENT_ICON_NAMES =
{
  "login" => "check-circle",
  "failed_login" => "x-circle",
  "logout" => "arrow-right-on-rectangle",
  "revoked" => "no-symbol",
  "expired" => "clock"
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.engine_mount_nameObject

The name of the route that mounts Sessions::Engine in the host app ("sessions" for a plain mount, the as: value otherwise). Memoized per-process; in development a changed mount name reloads routes and to_prepare re-touches this helper file, clearing the memo.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'app/helpers/sessions/engine_helper.rb', line 97

def self.engine_mount_name
  return @engine_mount_name if defined?(@engine_mount_name)

  @engine_mount_name = begin
    route = Rails.application.routes.routes.find do |candidate|
      # The mount sits behind a Constraints wrapper; unwrap a bounded
      # number of times, checking BEFORE each unwrap — Rails::Engine
      # itself responds to #app (its middleware stack), so an unguarded
      # `while app.respond_to?(:app)` would walk straight past it.
      app = candidate.app
      matched = false
      3.times do
        matched = (app == Sessions::Engine)
        break if matched || !app.respond_to?(:app)

        app = app.app
      end
      matched
    end
    route&.name
  rescue StandardError
    nil
  end
end

.reset_engine_mount_name!Object



122
123
124
# File 'app/helpers/sessions/engine_helper.rb', line 122

def self.reset_engine_mount_name!
  remove_instance_variable(:@engine_mount_name) if defined?(@engine_mount_name)
end

Instance Method Details

#sessions_device_icon(session) ⇒ Object



36
37
38
# File 'app/helpers/sessions/engine_helper.rb', line 36

def sessions_device_icon(session)
  DEVICE_ICONS.fetch(session.device_type.to_s, "🌐")
end

#sessions_device_icon_name(session) ⇒ Object



40
41
42
# File 'app/helpers/sessions/engine_helper.rb', line 40

def sessions_device_icon_name(session)
  DEVICE_ICON_NAMES.fetch(session.device_type.to_s, "globe-alt")
end

#sessions_devices_for(user, limit: nil) ⇒ Object

Semantic queries for custom/ejected views. Keep liveness and enumeration-safe history semantics in the gem, so host styling changes don't accidentally regress the data contract.



51
52
53
54
55
56
57
58
# File 'app/helpers/sessions/engine_helper.rb', line 51

def sessions_devices_for(user, limit: nil)
  scope = if user.respond_to?(:sessions)
            user.sessions.live.by_recency
          else
            Sessions.session_model.live.where(user: user).by_recency
          end
  limit ? scope.limit(limit) : scope
end

#sessions_engine_routesObject

The engine's route proxy when it's mounted, nil otherwise — partials rendered inside a host that didn't mount the engine simply omit the revoke buttons. The proxy method is named after the mount (sessions by default, or whatever as: was given), so it's DISCOVERED from the host's named routes rather than assumed.



88
89
90
91
# File 'app/helpers/sessions/engine_helper.rb', line 88

def sessions_engine_routes
  name = Sessions::EngineHelper.engine_mount_name
  name && respond_to?(name) ? public_send(name) : nil
end

#sessions_event_icon_name(event) ⇒ Object



44
45
46
# File 'app/helpers/sessions/engine_helper.rb', line 44

def sessions_event_icon_name(event)
  EVENT_ICON_NAMES.fetch(event.event.to_s, "information-circle")
end

#sessions_format_date(date) ⇒ Object

"Signed in May 2, 2026" — localized when the host bundles date formats (rails-i18n or its own locale files), with a safe fallback so a bare host never 500s over a missing date.formats.long. nil-safe: without the guard, I18n.l(nil) raises I18n::ArgumentError and the rescue would then call nil.strftime — a trap for custom views passing a nullable column.



132
133
134
135
136
137
138
# File 'app/helpers/sessions/engine_helper.rb', line 132

def sessions_format_date(date)
  return nil unless date

  I18n.l(date, format: :long)
rescue I18n::MissingTranslationData, I18n::ArgumentError
  date.strftime("%Y-%m-%d")
end

#sessions_format_time(time) ⇒ Object



140
141
142
143
144
145
146
# File 'app/helpers/sessions/engine_helper.rb', line 140

def sessions_format_time(time)
  return nil unless time

  I18n.l(time, format: :short)
rescue I18n::MissingTranslationData, I18n::ArgumentError
  time.strftime("%Y-%m-%d %H:%M")
end

#sessions_history_for(user, limit: nil) ⇒ Object



60
61
62
63
64
65
66
67
68
69
# File 'app/helpers/sessions/engine_helper.rb', line 60

def sessions_history_for(user, limit: nil)
  scope = if user.respond_to?(:session_history)
            user.session_history.recent
          elsif user.respond_to?(:session_events)
            user.session_events.recent
          else
            Sessions::Event.where(authenticatable: user).recent
          end
  limit ? scope.limit(limit) : scope
end

#sessions_last_active_in_words(session) ⇒ Object

"Active now" within the touch window, else "Active 3 minutes ago".



72
73
74
75
76
77
78
79
80
81
# File 'app/helpers/sessions/engine_helper.rb', line 72

def sessions_last_active_in_words(session)
  # active_now? owns the window (config.touch_every) — the badge can't
  # honestly claim more freshness than the throttle records.
  return t("sessions.devices.active_now") if session.respond_to?(:active_now?) && session.active_now?

  time = session.last_active_at
  return nil unless time

  t("sessions.devices.active_ago", time: time_ago_in_words(time))
end