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/
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
-
#desktop_app? ⇒ Boolean
True in the Tauri desktop shell.
-
#everywhere_auth_redirect(to) ⇒ Object
Where to send the user after an auth change.
-
#everywhere_badge(count) ⇒ Object
App icon badge count, server-rendered.
-
#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).
-
#everywhere_biometric_toggle(css_class: nil, id: nil) ⇒ Object
The settings switch for the device-local biometric lock.
-
#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).
-
#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.
-
#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.
-
#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.
-
#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).
-
#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).
-
#everywhere_tab_badge(path, count) ⇒ Object
Native tab bar badge for the tab whose everywhere.yml path is
path. -
#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.
-
#mobile_platform ⇒ Object
:ios, :android, or nil (including in the desktop shell).
-
#native_app? ⇒ Boolean
True inside any RubyEverywhere native shell — iOS, Android or desktop.
-
#native_platform ⇒ Object
:ios, :android, :desktop, or nil in a plain browser.
-
#native_version ⇒ Object
The shell's version as a Gem::Version (from the "RubyEverywhere/
" UA prefix), or nil outside the shell / when unparseable.
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.
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.(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()}") if }>#{_everywhere_attr()}</p>) if 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 (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 (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 (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 (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
200 201 202 203 204 |
# File 'lib/everywhere/native_helper.rb', line 200 def (title = "Save", side: "right", style: "done", form: nil, icon: nil, icons: nil, css_class: nil, id: nil) (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.(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".
50 51 52 |
# File 'lib/everywhere/native_helper.rb', line 50 def mobile_app? !mobile_platform.nil? end |
#mobile_platform ⇒ Object
: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.
30 31 32 |
# File 'lib/everywhere/native_helper.rb', line 30 def native_app? !native_platform.nil? end |
#native_platform ⇒ Object
: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_version ⇒ Object
The shell's version as a Gem::Version (from the "RubyEverywhere/
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 |