Module: Everywhere
- Defined in:
- lib/everywhere.rb,
lib/everywhere/s3.rb,
lib/everywhere/ui.rb,
lib/everywhere/cli.rb,
lib/everywhere/png.rb,
lib/everywhere/boot.rb,
lib/everywhere/dock.rb,
lib/everywhere/host.rb,
lib/everywhere/icon.rb,
lib/everywhere/jump.rb,
lib/everywhere/clock.rb,
lib/everywhere/error.rb,
lib/everywhere/fatal.rb,
lib/everywhere/paths.rb,
lib/everywhere/plist.rb,
lib/everywhere/relay.rb,
lib/everywhere/config.rb,
lib/everywhere/engine.rb,
lib/everywhere/ignore.rb,
lib/everywhere/raster.rb,
lib/everywhere/blake2b.rb,
lib/everywhere/console.rb,
lib/everywhere/raw_tty.rb,
lib/everywhere/receipt.rb,
lib/everywhere/version.rb,
lib/everywhere/database.rb,
lib/everywhere/emulator.rb,
lib/everywhere/minisign.rb,
lib/everywhere/omniauth.rb,
lib/everywhere/shellout.rb,
lib/everywhere/dock/info.rb,
lib/everywhere/framework.rb,
lib/everywhere/line_pump.rb,
lib/everywhere/simulator.rb,
lib/everywhere/task_pool.rb,
lib/everywhere/auth_token.rb,
lib/everywhere/config/app.rb,
lib/everywhere/dock/state.rb,
lib/everywhere/entrypoint.rb,
lib/everywhere/log_filter.rb,
lib/everywhere/tab_filter.rb,
lib/everywhere/android_sdk.rb,
lib/everywhere/config/auth.rb,
lib/everywhere/config/data.rb,
lib/everywhere/dock/footer.rb,
lib/everywhere/dock/screen.rb,
lib/everywhere/shell_pages.rb,
lib/everywhere/agents_guide.rb,
lib/everywhere/auth_handoff.rb,
lib/everywhere/builders/ios.rb,
lib/everywhere/commands/dev.rb,
lib/everywhere/config/shell.rb,
lib/everywhere/asset_catalog.rb,
lib/everywhere/builders/base.rb,
lib/everywhere/commands/icon.rb,
lib/everywhere/commands/logs.rb,
lib/everywhere/config/mobile.rb,
lib/everywhere/native_helper.rb,
lib/everywhere/commands/build.rb,
lib/everywhere/commands/clean.rb,
lib/everywhere/config/updates.rb,
lib/everywhere/desktop_assets.rb,
lib/everywhere/child_processes.rb,
lib/everywhere/commands/doctor.rb,
lib/everywhere/desktop_dev_app.rb,
lib/everywhere/native_platform.rb,
lib/everywhere/platform/client.rb,
lib/everywhere/request_context.rb,
lib/everywhere/update_manifest.rb,
lib/everywhere/builders/android.rb,
lib/everywhere/builders/desktop.rb,
lib/everywhere/commands/install.rb,
lib/everywhere/commands/preview.rb,
lib/everywhere/commands/publish.rb,
lib/everywhere/commands/release.rb,
lib/everywhere/android_resources.rb,
lib/everywhere/child_supervision.rb,
lib/everywhere/config/desktop_ui.rb,
lib/everywhere/platform/snapshot.rb,
lib/everywhere/commands/shell_dir.rb,
lib/everywhere/config/deep_linking.rb,
lib/everywhere/config/native_mobile.rb,
lib/everywhere/platform/credentials.rb,
lib/everywhere/config/native_desktop.rb,
lib/everywhere/mobile_config_endpoint.rb,
lib/everywhere/builders/native_sources.rb,
lib/everywhere/commands/platform/build.rb,
lib/everywhere/commands/platform/login.rb,
lib/everywhere/commands/updates_keygen.rb,
lib/everywhere/commands/platform/logout.rb,
lib/everywhere/commands/platform/runner.rb,
lib/everywhere/mobile_configs_controller.rb,
lib/everywhere/commands/platform/auth_status.rb
Defined Under Namespace
Modules: AgentsGuide, AndroidResources, AndroidSdk, AssetCatalog, AuthToken, Blake2b, Builders, CLI, ChildProcesses, ChildSupervision, Clock, Commands, Console, Database, DesktopAssets, Emulator, Entrypoint, Host, Icon, Jump, Minisign, NativeHelper, OmniAuth, PNG, Paths, Platform, Plist, RawTty, Shellout, Simulator, UI, UpdateManifest Classes: AuthHandoff, Boot, Config, Configuration, DesktopDevApp, Dock, Engine, Error, Fatal, Framework, Ignore, LinePump, LogFilter, MobileConfigEndpoint, MobileConfigsController, Raster, Receipt, Relay, RequestContext, S3, TaskPool
Constant Summary collapse
- VERSION =
"0.11.0"- BRIDGE_VERSION =
Version of the @rubyeverywhere/bridge JS this gem ships. bridge/ in the gem IS the npm package (served to Rails apps by Everywhere::Engine, vendored to public/ for Sinatra/Hanami), so its package.json is the single source of truth. The build receipt records it; it versions independently of the CLI.
JSON.parse( File.read(File.("../../bridge/package.json", __dir__)) )["version"]
- DESKTOP_UA =
The desktop shell's marker. WKWebView APPENDS
applicationNameForUserAgentto the real system UA, so this arrives alongside a normal Safari UA rather than replacing it — hence a pattern rather than a prefix check. The version is absent when everywhere.yml declares none.macOS only for now: the marker is set in macos_webview_configuration, and Windows/Linux have no append-only equivalent (their user_agent() REPLACES the string, and fabricating a whole UA is worse than not marking it). Desktop only ships macOS today; revisit when it doesn't.
%r{\bRubyEverywhere(?:/[\w.\-]+)? \((?:macos|windows|linux)\)}- MOBILE_PLATFORMS =
%i[ios android].freeze
Class Attribute Summary collapse
-
.current_user_resolver ⇒ Object
writeonly
Test/reset hooks.
-
.tabs_filter ⇒ Object
writeonly
Test/reset hooks.
Class Method Summary collapse
- .boot!(root:) ⇒ Object
- .config ⇒ Object
- .configure {|config| ... } ⇒ Object
-
.current_user(&block) ⇒ Object
Register how this app identifies the requester, once, for every hook that asks (
ctx.current_user). -
.filter_tabs(&block) ⇒ Object
The original filter-only form, still supported: the block takes the framework-native request rather than a context, and can only subset the list it's given.
-
.mobile_auth_html(to) ⇒ Object
The page served at /everywhere/auth/native.
-
.mobile_jump_leave_html ⇒ Object
The page served at /everywhere/jump/leave — the way OUT of a Jump preview.
-
.mobile_platform_of(user_agent) ⇒ Object
Just the phone shells.
-
.mobile_reset_html(to) ⇒ Object
The tiny page served at /everywhere/reset.
-
.native_platform_of(user_agent) ⇒ Object
:ios, :android, :desktop or nil, from a User-Agent string.
-
.reset_config! ⇒ Object
Test/reset seam.
-
.resolve_current_user(context) ⇒ Object
nil when no resolver is registered — hooks treat that as signed out.
-
.resolve_tabs(tabs, request, platform: nil, config: nil) ⇒ Object
Apply the registered tab hook (identity when none is set).
-
.tabs(&block) ⇒ Object
Register the per-request tab bar hook.
Class Attribute Details
.current_user_resolver=(value) ⇒ Object (writeonly)
Test/reset hooks.
93 94 95 |
# File 'lib/everywhere/tab_filter.rb', line 93 def current_user_resolver=(value) @current_user_resolver = value end |
.tabs_filter=(value) ⇒ Object (writeonly)
Test/reset hooks.
93 94 95 |
# File 'lib/everywhere/tab_filter.rb', line 93 def tabs_filter=(value) @tabs_filter = value end |
Class Method Details
.boot!(root:) ⇒ Object
185 186 187 |
# File 'lib/everywhere/boot.rb', line 185 def self.boot!(root:) Boot.call(root: root) end |
.config ⇒ Object
39 40 41 |
# File 'lib/everywhere.rb', line 39 def config @config ||= Configuration.new end |
.configure {|config| ... } ⇒ Object
43 44 45 |
# File 'lib/everywhere.rb', line 43 def configure yield config end |
.current_user(&block) ⇒ Object
Register how this app identifies the requester, once, for every hook that
asks (ctx.current_user). The mobile config endpoint deliberately doesn't
inherit ApplicationController — its before_actions would lock the shells
out — so it can't reach Current or a current_user helper, and this is
where the app closes that gap:
# config/initializers/everywhere.rb
Rails.application.config.to_prepare do
Everywhere.current_user do |ctx|
session_id = ctx..signed[:session_id]
Session.find_by(id: session_id)&.user if session_id
end
end
Devise and anything else on Warden need no cookie work at all:
Everywhere.current_user { |ctx| ctx.request.env["warden"]&.user }
Return nil for a signed-out request. Runs at most once per request.
69 70 71 72 73 74 75 76 |
# File 'lib/everywhere/tab_filter.rb', line 69 def current_user(&block) unless block raise ArgumentError, "Everywhere.current_user needs a block — it registers the resolver " \ "(ask a RequestContext for the user itself: ctx.current_user)" end @current_user_resolver = block end |
.filter_tabs(&block) ⇒ Object
The original filter-only form, still supported: the block takes the framework-native request rather than a context, and can only subset the list it's given. #tabs is the one to write today.
44 45 46 47 48 |
# File 'lib/everywhere/tab_filter.rb', line 44 def filter_tabs(&block) raise ArgumentError, "Everywhere.filter_tabs needs a block" unless block @tabs_filter = ->(tabs, context) { block.call(tabs, context.request) } end |
.mobile_auth_html(to) ⇒ Object
The page served at /everywhere/auth/native. The shell normally diverts a
provider path natively, before the request is ever made; this covers the
visits it can't see — a data-turbo="false" link, or the POST OmniAuth 2
requires — by asking the shell, from the page, to open the auth session.
In a browser it just continues to the provider.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/everywhere/shell_pages.rb', line 88 def mobile_auth_html(to) require "json" target = to.to_s target = "/" unless target.start_with?("/") && !target.start_with?("//") # script_safe, as in mobile_reset_html: `to` is request input. encoded = JSON.generate(target, script_safe: true) <<~HTML <!DOCTYPE html><html><head><meta charset="utf-8"><title>Signing in…</title> <meta name="viewport" content="width=device-width,initial-scale=1"></head> <body><p>Opening secure sign-in…</p><script> (function(){var to=#{encoded};var msg={action:"authFlow",to:to}; var ios=window.webkit&&window.webkit.messageHandlers&&window.webkit.messageHandlers.everywhereControl; var android=window.everywhereControl; if(ios){ios.postMessage(msg);} else if(android&&android.postMessage){android.postMessage(JSON.stringify(msg));} else{window.location.replace(to);}})(); </script></body></html> HTML end |
.mobile_jump_leave_html ⇒ Object
The page served at /everywhere/jump/leave — the way OUT of a Jump preview. Once Jump re-roots onto a previewed app (instance.set), every Jump surface — launcher, scanner, tabs — resolves against the previewed app, so the escape hatch has to be served by the previewed app itself: this gem is the one thing guaranteed to be there. Posts clearInstance on the shell's control channel; shells that aren't multi-instance ignore it, and a plain browser just goes home. Fires clearInstance only once VISIBLE. This page rides in the previewed app's tab bar; shells preload tabs eagerly, but a preloaded webview is offscreen and reports visibilityState "hidden" — firing on load would bounce every preview the instant it connects, and a tap on the tab never re-proposes a visit, so visibility flipping is the ONLY signal a tab tap gives the page. The button is a belt for browsers and any webview that lies about visibility.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/everywhere/shell_pages.rb', line 53 def mobile_jump_leave_html <<~HTML <!DOCTYPE html><html><head><meta charset="utf-8"><title>Jump</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> body{font-family:-apple-system,system-ui,sans-serif;margin:0;min-height:100vh; display:flex;flex-direction:column;align-items:center;justify-content:center; gap:12px;padding:24px;text-align:center;background:#fff;color:#171717} @media(prefers-color-scheme:dark){body{background:#171717;color:#fafafa}} p{margin:0;opacity:.6} button{font:inherit;font-weight:600;font-size:18px;color:#fff;background:#dc2626; border:0;border-radius:12px;padding:16px 32px} </style></head> <body> <p>Returning to Jump…</p> <button onclick="everywhereJumpLeave()">Return to Jump</button> <script> function everywhereJumpLeave(){var msg={action:"clearInstance"}; var ios=window.webkit&&window.webkit.messageHandlers&&window.webkit.messageHandlers.everywhereControl; var android=window.everywhereControl; if(ios){ios.postMessage(msg);} else if(android&&android.postMessage){android.postMessage(JSON.stringify(msg));} else{window.location.replace("/");}} if(document.visibilityState==="visible"){everywhereJumpLeave();} else{document.addEventListener("visibilitychange",function(){ if(document.visibilityState==="visible"){everywhereJumpLeave();}});} </script></body></html> HTML end |
.mobile_platform_of(user_agent) ⇒ Object
Just the phone shells. Anything that leans on a mobile-only affordance — the OAuth handoff to ASWebAuthenticationSession / Custom Tabs, biometric gating — has to ask for this rather than "is this a native shell", because the desktop shell answers yes to that and has neither.
39 40 41 42 |
# File 'lib/everywhere/native_platform.rb', line 39 def mobile_platform_of(user_agent) platform = native_platform_of(user_agent) platform if MOBILE_PLATFORMS.include?(platform) end |
.mobile_reset_html(to) ⇒ Object
The tiny page served at /everywhere/reset. Auth flows redirect the native
app here so it resets cleanly (fresh web views, re-fetched tabs) before
continuing — the standard Hotwire Native "reset the app" pattern. It talks
to the shell's control channel directly (no bridge/importmap dependency);
in a plain browser it just forwards to the target. to is constrained to
a same-origin path.
Both shells are addressed inline, and differently, for the same reason the bridge normalizes them: WKWebView's message handler takes an object, while Android's WebMessageListener channel takes a string. Reaching for the bridge here instead would trade that one line for an importmap dependency on a page whose whole job is to work before anything loads.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/everywhere/shell_pages.rb', line 17 def mobile_reset_html(to) require "json" target = to.to_s target = "/" unless target.start_with?("/") && !target.start_with?("//") # script_safe, not to_json: this lands inside <script>, and outside Rails # nothing escapes "</script>" in a plain JSON string. encoded = JSON.generate(target, script_safe: true) <<~HTML <!DOCTYPE html><html><head><meta charset="utf-8"><title>One moment…</title> <meta name="viewport" content="width=device-width,initial-scale=1"></head> <body><script> (function(){var to=#{encoded};var msg={action:"reset",to:to}; var ios=window.webkit&&window.webkit.messageHandlers&&window.webkit.messageHandlers.everywhereControl; var android=window.everywhereControl; if(ios){ios.postMessage(msg);} else if(android&&android.postMessage){android.postMessage(JSON.stringify(msg));} else{window.location.replace(to);}})(); </script></body></html> HTML end |
.native_platform_of(user_agent) ⇒ Object
:ios, :android, :desktop or nil, from a User-Agent string. Present from the very first request, before any JS has run. Shared by the view helpers and the auth middleware.
Mobile is checked FIRST and the order matters: the mobile shells prepend
the same "RubyEverywhere/
26 27 28 29 30 31 32 33 |
# File 'lib/everywhere/native_platform.rb', line 26 def native_platform_of(user_agent) ua = user_agent.to_s return :ios if ua.include?("Hotwire Native iOS") return :android if ua.include?("Hotwire Native Android") return :desktop if ua.match?(DESKTOP_UA) nil end |
.reset_config! ⇒ Object
Test/reset seam.
48 49 50 |
# File 'lib/everywhere.rb', line 48 def reset_config! @config = Configuration.new end |
.resolve_current_user(context) ⇒ Object
nil when no resolver is registered — hooks treat that as signed out.
88 89 90 |
# File 'lib/everywhere/tab_filter.rb', line 88 def resolve_current_user(context) @current_user_resolver&.call(context) end |
.resolve_tabs(tabs, request, platform: nil, config: nil) ⇒ Object
Apply the registered tab hook (identity when none is set). Always returns an Array so a stray nil/scalar from a block can't break serialization.
80 81 82 83 84 85 |
# File 'lib/everywhere/tab_filter.rb', line 80 def resolve_tabs(tabs, request, platform: nil, config: nil) return tabs unless @tabs_filter context = RequestContext.new(request, platform: platform, config: config) Array(@tabs_filter.call(tabs, context)) end |
.tabs(&block) ⇒ Object
Register the per-request tab bar hook. The block receives the tabs
config/everywhere.yml declares ([{ "title" =>, "path" =>, "icon" => }, …],
already resolved for the asking platform) and an Everywhere::RequestContext,
and returns the list to serve — return [] to hide the tab bar entirely
(the shell falls back to single-screen navigation).
The list it returns is the list, so this adds and reorders as well as hides. Anything the app can answer during a request can decide it:
# config/initializers/everywhere.rb
Rails.application.config.to_prepare do
Everywhere.tabs do |tabs, ctx|
next [] unless ctx.current_user
tabs = tabs.reject { |tab| tab["title"] == "Labs" } unless
Flipper.enabled?(:labs, ctx.current_user)
tabs << ctx.tab("Admin") if ctx.current_user.admin?
tabs
end
end
ctx.tab("Admin") pulls in a tab declared visible: false — see
RequestContext#tab. ctx.current_user needs a resolver; see #current_user.
Live like the rest of the config: the shell re-reads it on launch and on
every foreground, so a sign-in shows the tabs on next foreground — no
rebuild, no app-store release. Wrap the registration in to_prepare so a
reload in development doesn't leave the block closed over stale constants.
35 36 37 38 39 |
# File 'lib/everywhere/tab_filter.rb', line 35 def tabs(&block) raise ArgumentError, "Everywhere.tabs needs a block — Everywhere.tabs { |tabs, ctx| … }" unless block @tabs_filter = block end |