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 native_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.

Instance Method Summary collapse

Instance Method Details

#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.



55
56
57
58
59
60
61
# File 'lib/everywhere/native_helper.rb', line 55

def everywhere_auth_redirect(to)
  return to unless native_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.



71
72
73
# File 'lib/everywhere/native_helper.rb', line 71

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:



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/everywhere/native_helper.rb', line 105

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
  return content unless native_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:



135
136
137
138
139
140
141
# File 'lib/everywhere/native_helper.rb', line 135

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 %>


239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/everywhere/native_helper.rb', line 239

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 %>


218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/everywhere/native_helper.rb', line 218

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.



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

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" %>


153
154
155
156
157
158
159
160
# File 'lib/everywhere/native_helper.rb', line 153

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 %>


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

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 %>


170
171
172
173
174
# File 'lib/everywhere/native_helper.rb', line 170

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 %>


79
80
81
82
# File 'lib/everywhere/native_helper.rb', line 79

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

#native_app?Boolean

True inside any RubyEverywhere native shell (iOS or Android).

Returns:

  • (Boolean)


21
22
23
# File 'lib/everywhere/native_helper.rb', line 21

def native_app?
  !native_platform.nil?
end

#native_platformObject

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



27
28
29
30
31
32
33
# File 'lib/everywhere/native_helper.rb', line 27

def native_platform
  ua = _everywhere_user_agent or return nil
  return :ios if ua.include?("Hotwire Native iOS")
  return :android if ua.include?("Hotwire Native Android")

  nil
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.



38
39
40
41
42
43
44
45
# File 'lib/everywhere/native_helper.rb', line 38

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