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/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/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, S3, TaskPool

Constant Summary collapse

VERSION =
"0.8.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.expand_path("../../bridge/package.json", __dir__))
)["version"]
DESKTOP_UA =

The desktop shell's marker. WKWebView APPENDS applicationNameForUserAgent to 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

Class Method Summary collapse

Class Attribute Details

.tabs_filter=(value) ⇒ Object (writeonly)

Test/reset hook.



34
35
36
# File 'lib/everywhere/tab_filter.rb', line 34

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

.configObject



39
40
41
# File 'lib/everywhere.rb', line 39

def config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Yields:



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

def configure
  yield config
end

.filter_tabs(&block) ⇒ Object

Register a per-request filter for the mobile tab bar. The block receives the resolved tab list ([{ "title" =>, "path" =>, "icon" => }, …]) and the current request, and returns the subset to show — return [] to hide the tab bar entirely (the shell falls back to single-screen navigation).

It runs inside the mobile config endpoint, which shares the app's session and cookies, so it can branch on auth or any request state:

# config/initializers/everywhere.rb
Everywhere.filter_tabs do |tabs, request|
request.session[:user_id] ? tabs : []
end

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.



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

def filter_tabs(&block)
  @tabs_filter = block
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_htmlObject

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/ ()" marker to Hotwire Native's own, so a mobile UA matches DESKTOP_UA too.



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_tabs(tabs, request) ⇒ Object

Apply the registered filter (identity when none is set). Always returns an Array so a stray nil/scalar from a block can't break serialization.



27
28
29
30
31
# File 'lib/everywhere/tab_filter.rb', line 27

def resolve_tabs(tabs, request)
  return tabs unless @tabs_filter

  Array(@tabs_filter.call(tabs, request))
end