Module: Everywhere::NativeHelper

Defined in:
lib/everywhere/native_helper.rb

Overview

Server-side view helpers mirroring the JS bridge's platform detection, so ERB can branch on the native shell the same way Everywhere.platform does on the client:

<%= link_to "Install the app", … unless native_app? %>
<div class="<%= "native-inset" if mobile_app? %>">
<% if native_version && native_version >= Gem::Version.new("1.2") %>

Detection is by User-Agent: the RubyEverywhere shells prepend "RubyEverywhere/ ()" to Hotwire Native's own "Hotwire Native iOS/Android" marker. Included into all Rails views by Everywhere::Engine.

Pick the narrowest one that's true of what you're branching on. native_app? means "not a browser tab" and covers the desktop shell; mobile_app? means a phone, with a tab bar and safe-area insets; desktop_app? means a window, with a title bar and a menu bar. Safe-area padding is mobile_app?; hiding a web nav in favour of native chrome is usually mobile_app? too — the desktop shell has no native navigation to replace it with.

Instance Method Summary collapse

Instance Method Details

#desktop_app?Boolean

True in the Tauri desktop shell. Use it for window chrome — reserving room for an overlay title bar, say — and for anything that assumes a pointer and a resizable window.

Returns:

  • (Boolean)


43
44
45
# File 'lib/everywhere/native_helper.rb', line 43

def desktop_app?
  native_platform == :desktop
end

#everywhere_auth_redirect(to) ⇒ Object

Where to send the user after an auth change. In a native shell, route through the reset page (/everywhere/reset) so the app rebuilds cleanly — fresh web views, re-fetched tabs — then lands on to. In a browser it's just to. Use it in controllers:

redirect_to everywhere_auth_redirect(after_authentication_url)

to may be a full URL or a path; only the same-origin path is forwarded. mobile_app?, not native_app?: the reset page exists to rebuild a phone shell's web views and re-fetch its tab bar. The desktop shell has neither, so a plain redirect is both correct and what it already did.



82
83
84
85
86
87
88
# File 'lib/everywhere/native_helper.rb', line 82

def everywhere_auth_redirect(to)
  return to unless mobile_app? && respond_to?(:everywhere_reset_path)

  path = to.to_s.sub(%r{\Ahttps?://[^/]+}, "")
  path = "/" if path.empty? || !path.start_with?("/")
  everywhere_reset_path(to: path)
end

#everywhere_badge(count) ⇒ Object

App icon badge count, server-rendered. Emits a meta tag the JS bridge applies on every Turbo visit — CSP-safe (no inline script) and correct on the first paint. 0 clears the badge. Put it in the layout :

<%= everywhere_badge Current.user.unread_count %>

In the mobile shell this badges the app icon; in an installed PWA the Badging API; elsewhere it renders inert metadata.



98
99
100
# File 'lib/everywhere/native_helper.rb', line 98

def everywhere_badge(count)
  tag.meta(name: "everywhere:badge", content: count.to_i)
end

#everywhere_biometric_lock(reason: nil, name: nil, allow_passcode: true, unlock_label: "Unlock", locked_message: nil, wrapper_class: nil, locked_class: nil, message_class: nil, unlock_class: nil, &block) ⇒ Object

Biometric gate: wraps sensitive markup so the mobile shell keeps it hidden until Face ID / Touch ID passes — but only when the user has the device-local lock turned on (everywhere_biometric_toggle, or Everywhere.biometrics.setLockEnabled). Browsers and the desktop shell render the content plainly. Requires biometrics in everywhere.yml's permissions.

<%= everywhere_biometric_lock reason: "Unlock account settings" do %>
...profile, password, sessions...
<% end %>

name keys the once-per-session unlock (defaults to the page path). For a fully custom overlay, hand-write the contract this emits:



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/everywhere/native_helper.rb', line 132

def everywhere_biometric_lock(reason: nil, name: nil, allow_passcode: true,
                              unlock_label: "Unlock", locked_message: nil,
                              wrapper_class: nil, locked_class: nil,
                              message_class: nil, unlock_class: nil, &block)
  content = respond_to?(:capture) ? capture(&block) : block.call
  # mobile_app?: there's no Face ID on the desktop shell, so emitting the
  # locked wrapper there would hide the content behind an unlock nothing can
  # satisfy. Browsers and the desktop shell render it plainly.
  return content unless mobile_app?

  attrs = [%(data-everywhere-biometric-lock="#{_everywhere_attr(name)}")]
  attrs << %(data-everywhere-biometric-reason="#{_everywhere_attr(reason)}") if reason
  attrs << "data-everywhere-biometric-passcode" if allow_passcode
  attrs << %(class="#{_everywhere_attr(wrapper_class)}") if wrapper_class

  html = +"<div #{attrs.join(" ")}>"
  html << %(<div data-everywhere-biometric-content hidden>#{content}</div>)
  html << %(<div data-everywhere-biometric-locked hidden#{%( class="#{_everywhere_attr(locked_class)}") if locked_class}>)
  html << %(<p#{%( class="#{_everywhere_attr(message_class)}") if message_class}>#{_everywhere_attr(locked_message)}</p>) if locked_message
  html << %(<button type="button" data-everywhere-biometric-unlock#{%( class="#{_everywhere_attr(unlock_class)}") if unlock_class}>#{_everywhere_attr(unlock_label)}</button>)
  html << "</div></div>"
  html.respond_to?(:html_safe) ? html.html_safe : html
end

#everywhere_biometric_toggle(css_class: nil, id: nil) ⇒ Object

The settings switch for the device-local biometric lock. Renders hidden and disabled; the bridge reveals and enables it only in the mobile shell with working biometrics (toggling runs the biometric check first). Put data-everywhere-biometric-toggle-row + hidden on the surrounding row to reveal the whole thing together:



165
166
167
168
169
170
171
# File 'lib/everywhere/native_helper.rb', line 165

def everywhere_biometric_toggle(css_class: nil, id: nil)
  attrs = +""
  attrs << %( id="#{_everywhere_attr(id)}") if id
  attrs << %( class="#{_everywhere_attr(css_class)}") if css_class
  html = %(<input type="checkbox" hidden disabled data-everywhere-biometric-toggle#{attrs}>)
  html.respond_to?(:html_safe) ? html.html_safe : html
end

#everywhere_fab(href = nil, icon: :plus, label: nil, haptic: "light", side: "right", extended: false, type: nil, form: nil, method: nil, css_class: nil, id: nil, &block) ⇒ Object

A floating action button: a fixed, safe-area-aware circular control (see everywhere/native.css). A real link (or button) that shows in a browser too and gains a tap haptic in the shell. Pass a block for custom content, or icon: for a built-in line glyph; label: sets the accessible name and, with extended: true, a visible label pill.

<%= everywhere_fab new_note_path, icon: :plus, label: "New note" %>
<%= everywhere_fab compose_path, icon: :pencil, label: "Compose", extended: true %>


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/everywhere/native_helper.rb', line 269

def everywhere_fab(href = nil, icon: :plus, label: nil, haptic: "light", side: "right",
                   extended: false, type: nil, form: nil, method: nil,
                   css_class: nil, id: nil, &block)
  classes = ["everywhere-fab"]
  classes << "everywhere-fab-left" if side.to_s == "left"
  classes << "everywhere-fab-extended" if extended
  classes << css_class if css_class

  inner = block ? _everywhere_capture(&block) : _everywhere_fab_icon(icon)
  inner = "#{inner}<span class=\"everywhere-fab-label\">#{_everywhere_attr(label)}</span>" if extended && label

  extra = %(class="#{classes.join(" ")}")
  extra << %( aria-label="#{_everywhere_attr(label)}") if label
  extra << %( data-everywhere-haptic="#{_everywhere_attr(haptic)}") if haptic

  _everywhere_clickable(inner, href, type: type, form: form, method: method,
                        id: id, extra: extra, raw_content: true)
end

#everywhere_menu(trigger = "Options", title: nil, trigger_class: nil, items_class: nil, css_class: nil, id: nil, &block) ⇒ Object

An in-content action sheet: a trigger and the items it opens. In the mobile shell the trigger presents a native action sheet; in a browser it toggles the items as an inline menu (styled by everywhere/native.css). Choosing an item clicks it, so behavior lives in the items themselves.

<%= everywhere_menu "Options" do %>
<%= everywhere_menu_item "Edit", edit_post_path(@post) %>
<%= everywhere_menu_item "Delete", post_path(@post), method: :delete, style: "destructive" %>
<% end %>


248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/everywhere/native_helper.rb', line 248

def everywhere_menu(trigger = "Options", title: nil, trigger_class: nil,
                    items_class: nil, css_class: nil, id: nil, &block)
  content = _everywhere_capture(&block)
  wrap = +""
  wrap << %( id="#{_everywhere_attr(id)}") if id
  wrap << %( class="#{_everywhere_attr(css_class)}") if css_class
  wrap << %( data-everywhere-menu-title="#{_everywhere_attr(title)}") if title

  trig = %(<button type="button" data-everywhere-menu-trigger#{%( class="#{_everywhere_attr(trigger_class)}") if trigger_class}>#{_everywhere_attr(trigger)}</button>)
  items = %(<div data-everywhere-menu-items#{%( class="#{_everywhere_attr(items_class)}") if items_class}>#{content}</div>)
  _everywhere_safe(%(<div data-everywhere-menu#{wrap}>#{trig}#{items}</div>))
end

#everywhere_menu_item(title, href = nil, icon: nil, icons: nil, style: nil, type: nil, form: nil, method: nil, css_class: nil, id: nil) ⇒ Object

One item inside an everywhere_nav_menu or everywhere_menu. A link by default; type: :submit (with form:) submits a form, and method: (:delete, …) makes a Turbo method link. style: "destructive" tints it in the native menu / action sheet.



229
230
231
232
233
234
235
236
237
# File 'lib/everywhere/native_helper.rb', line 229

def everywhere_menu_item(title, href = nil, icon: nil, icons: nil, style: nil,
                         type: nil, form: nil, method: nil, css_class: nil, id: nil)
  extra = +"data-everywhere-menu-item"
  extra << %( data-everywhere-menu-title="#{_everywhere_attr(title)}")
  extra << _everywhere_icon_attrs("data-everywhere-menu-icon", icon, icons)
  extra << %( data-everywhere-menu-style="#{_everywhere_attr(style)}") if style
  _everywhere_clickable(title, href, type: type, form: form, method: method,
                        css_class: css_class, id: id, extra: extra)
end

#everywhere_nav_button(title, href = nil, icon: nil, icons: nil, side: "right", style: "plain", type: nil, form: nil, disabled: false, method: nil, css_class: nil, id: nil) ⇒ Object

A navigation-bar button. Renders a normal link (or a submit button with type: :submit) that the mobile shell lifts into the top navigation bar — tapping the native button just clicks this element, so a link navigates and a submit submits, defined once. In a browser it's the plain link it already is (the shell hides the in-page copy). Icons are per-platform like tabs: icon: is the shared fallback, icons: android: the specifics (SF Symbol / Material name).

<%= everywhere_nav_button "New", new_note_path, icons: { ios: "plus", android: "add" } %>
<%= everywhere_nav_button "Save", type: :submit, style: "done" %>


183
184
185
186
187
188
189
190
# File 'lib/everywhere/native_helper.rb', line 183

def everywhere_nav_button(title, href = nil, icon: nil, icons: nil, side: "right",
                          style: "plain", type: nil, form: nil, disabled: false,
                          method: nil, css_class: nil, id: nil)
  nav = _everywhere_nav_attrs("data-everywhere-nav-button", title: title, icon: icon,
                              icons: icons, side: side, style: style, disabled: disabled)
  _everywhere_clickable(title, href, type: type, form: form, method: method,
                        disabled: disabled, css_class: css_class, id: id, extra: nav)
end

#everywhere_nav_menu(title = nil, icon: nil, icons: nil, side: "right", css_class: nil, id: nil, &block) ⇒ Object

A navigation-bar pull-down / overflow menu (defaults to the ⋯ ellipsis icon). Yields the items — build them with everywhere_menu_item; in the shell they become a native UIMenu, in a browser a plain list of links.

<%= everywhere_nav_menu do %>
<%= everywhere_menu_item "Share", share_path, icons: { ios: "square.and.arrow.up" } %>
<%= everywhere_menu_item "Delete", note_path(@note), method: :delete, style: "destructive" %>
<% end %>


214
215
216
217
218
219
220
221
222
223
# File 'lib/everywhere/native_helper.rb', line 214

def everywhere_nav_menu(title = nil, icon: nil, icons: nil, side: "right",
                        css_class: nil, id: nil, &block)
  content = _everywhere_capture(&block)
  attrs = _everywhere_nav_attrs("data-everywhere-nav-menu", title: title, icon: icon,
                                icons: icons, side: side)
  common = +""
  common << %( id="#{_everywhere_attr(id)}") if id
  common << %( class="#{_everywhere_attr(css_class)}") if css_class
  _everywhere_safe(%(<div#{common} #{attrs}>#{content}</div>))
end

#everywhere_submit_button(title = "Save", side: "right", style: "done", form: nil, icon: nil, icons: nil, css_class: nil, id: nil) ⇒ Object

A form's submit control mirrored into the navigation bar (right side, "done" style by default). Place it inside the

, or aim it at one with form: "".

<%= form_with model: @note do |f| %>
<%= everywhere_submit_button "Save" %>
...
<% end %>


200
201
202
203
204
# File 'lib/everywhere/native_helper.rb', line 200

def everywhere_submit_button(title = "Save", side: "right", style: "done", form: nil,
                             icon: nil, icons: nil, css_class: nil, id: nil)
  everywhere_nav_button(title, type: :submit, side: side, style: style, form: form,
                        icon: icon, icons: icons, css_class: css_class, id: id)
end

#everywhere_tab_badge(path, count) ⇒ Object

Native tab bar badge for the tab whose everywhere.yml path is path. Same delivery as everywhere_badge; 0 clears. Mobile shell only.

<%= everywhere_tab_badge "/inbox", Current.user.unread_count %>


106
107
108
109
# File 'lib/everywhere/native_helper.rb', line 106

def everywhere_tab_badge(path, count)
  tag.meta(name: "everywhere:tab-badge",
           content: JSON.generate({ path: path, count: count.to_i }))
end

#mobile_app?Boolean

True in the iOS or Android shell, and the one to reach for when the affordance is a phone's: safe-area insets, the native tab bar, biometrics, the OAuth handoff. Mirrors the bridge's Everywhere.platform === "mobile".

Returns:

  • (Boolean)


50
51
52
# File 'lib/everywhere/native_helper.rb', line 50

def mobile_app?
  !mobile_platform.nil?
end

#mobile_platformObject

:ios, :android, or nil (including in the desktop shell).



55
56
57
# File 'lib/everywhere/native_helper.rb', line 55

def mobile_platform
  Everywhere.mobile_platform_of(_everywhere_user_agent)
end

#native_app?Boolean

True inside any RubyEverywhere native shell — iOS, Android or desktop. Mirrors the bridge's Everywhere.native.

Returns:

  • (Boolean)


30
31
32
# File 'lib/everywhere/native_helper.rb', line 30

def native_app?
  !native_platform.nil?
end

#native_platformObject

:ios, :android, :desktop, or nil in a plain browser. Reads the shell's UA marker, so it's correct even before the JS bridge has booted.



36
37
38
# File 'lib/everywhere/native_helper.rb', line 36

def native_platform
  Everywhere.native_platform_of(_everywhere_user_agent)
end

#native_versionObject

The shell's version as a Gem::Version (from the "RubyEverywhere/" UA prefix), or nil outside the shell / when unparseable. Use it to gate features that need a newer shell than some users have installed.



62
63
64
65
66
67
68
69
# File 'lib/everywhere/native_helper.rb', line 62

def native_version
  ua = _everywhere_user_agent or return nil
  match = ua.match(%r{RubyEverywhere/([\w.\-]+)}) or return nil

  Gem::Version.new(match[1])
rescue ArgumentError
  nil
end