All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
ruby_native previewnow takes its port fromPORTwhen that is set, matchingrails server.--portstill wins, and an unusablePORTfalls back to 3000. Thanks, @jnstq!- Taps feel native out of the box. In the app, the web view's gray tap flash is gone, long-pressing a button no longer selects its label, and double-tap zoom is off on links, buttons, and form controls. Pressed states you style with
:activenow apply instantly on touch instead of after a long hold. Both platforms, both modes. - The User-Agent now reports the app's version, build number, and OS version, like
Ruby Native iOS/5.2/35 iOS/26.5.2 RubyNative/0.12.5. New helpers read them:native_app_version,native_app_build, andnative_os_version. Each isnilon the web; build and OS are alsonilin apps built before this release. Both platforms, both modes. Docs RubyNative.toast()accepts per-platform icons, like the helper always has:RubyNative.toast({ message: "Shared.", icons: { ios: "square.and.arrow.up", android: "share" } }). Both platforms, both modes.- Linked domains now work on Android: links and QR codes to your site open the app, and saved website passwords fill in the app. Add the
androidsection toconfig/ruby_native.ymland ship a build after this release. Docs - Link only part of your domain with
linked_paths. List path prefixes inconfig/ruby_native.ymland only those URLs open the app; everything else keeps opening in the browser. Both platforms. Docs - New CSS variables keep fixed overlays out of the status bar. Banners, toasts, and modal headers anchored with
top:can use--ruby-native-safe-area-top(and-bottom) in amax()expression to clear the notch on iOS and the status bar on Android, where rawenv(safe-area-inset-*)silently returns 0. Both platforms, both modes. Docs
rails generate ruby_native:installnow works in apps with a customized development config. Apps that disable host checking withconfig.hosts = nilno longer crash on boot after installing, and apps whose development.rb uses a renamed configure block get the tunnel host added instead of silently skipped.
- Support tablets. Turn it on in your app's dashboard settings and iOS builds target iPad as well as iPhone, which also lists the app on Apple Vision Pro. Required when you're replacing an App Store app that already supports iPad: Apple rejects any update that drops a device. It can't be turned off once an iPad build ships, so the dashboard locks it after. Android tablets are always included, no setting needed.
- Tablets now rotate freely. The portrait-by-default rule applies to phones only;
appearance.landscapestill controls them. Newer OS versions ignore orientation locks on large screens anyway, so locking only split behavior across older tablets. Both platforms, both modes. - A rejected App Store Connect upload now shows Apple's actual reason on the build page, instead of pointing you at your API key when the key was fine.
- A modal form's redirect now renders the page it returns to. The landing page kept its stale pre-submit content, and a flash toast on it never showed. Advanced Mode, both platforms.
- Navbar buttons without an icon now show their title as text on Android, matching iOS. They rendered the missing-icon placeholder before. Both modes.
- The floating action button now carries its
rubyNative/fabaccessibility identifier, so UI tests can address it. Advanced Mode, iOS.
- Native toasts.
<%= native_toast_tag flash[:notice] %>in your layout floats confirmations over all app chrome, including the nav bar, and navigating back never repeats one. Both platforms, both modes. Docs, including the Inertia components. RubyNative.toast("Saved.")shows a toast from client-side JavaScript, no server round trip. A browser ignores the call, so it is safe to run unconditionally.
ruby_native previewnow names the real reason your app did not serve its config. Every failure used to blame an unmounted gem and link a docs page that does not exist, so a 500 from a pending migration sent you to re-check a mount that was already fine. A 500 now points at your app, and a redirect names where the request went.
- A leading navbar button now yields its slot to the back button. One rule on every platform and mode: the button shows on screens with nothing to go back to, like a tab's root. If you rely on a leading button staying visible on pushed screens (previously iOS Normal Mode only), move it to
position: :trailing. Before, iOS Normal Mode showed both, iOS Advanced Mode lost its back button behind the leading button, and Android dropped the button outside a tab root. - Android apps are now portrait-only by default, matching iOS. Android previously ignored
appearance.landscapeand always rotated. If your app should rotate, setlandscape: trueand rebuild; rotation then resizes in place instead of restarting the screen.
- Signing out or switching accounts now resets the app, clearing every tab, once you opt in with the identity tag. Add
<%= native_identity_tag current_user&.id %>to your layout; signing in never resets, and apps without the tag keep today's behavior. Docs, including the Inertia setup. - Links to another tab's path now switch tabs on Android in Normal Mode, matching iOS for Turbo, Inertia, and plain links.
- The Android back button now follows your app's navigation history in Normal Mode, skipping form pages.
- Hiding the tab bar no longer loses the page in Advanced Mode on Android. The iOS half shipped in 0.11.2.
- Builds and screenshot captures now use the native code that matches your gem version. Patch fixes still arrive automatically; a new native minor arrives when you update the gem.
- Navbar segments now render on Android as a Material segmented control in both modes, closing the gap with iOS for Turbo and Inertia apps.
- Share buttons and share menu items now work on Android in both modes, opening the system share sheet.
- Submitting a form now refreshes the other tabs on Android the next time you open them, so lists no longer show pre-submit data. Normal Mode; iOS already did this.
- The navbar save button on Android now enables and disables live as the form changes, matching iOS.
- Re-tapping the current tab on Android now returns it to its root, matching iOS.
- Leading navbar buttons now render in Advanced Mode on Android, which previously never showed them.
- Tabs marked
eager: truenow preload on Android, matching iOS. - Android apps now cold-start offline using the last known configuration, matching iOS.
- CSS keyed on
body.can-go-backnow activates on Android in Normal Mode. - The Android error screen now names a gem/app version mismatch with "Gem update needed" or "App update needed" and the fix, matching iOS. Demo app only.
- Backgrounding an Android app now pauses video, audio, and page timers, matching iOS and saving battery.
- Android release builds are now minified, shrinking the APK and stripping debug logging.
- A crafted
ruby_native loginlink can no longer hand your CLI token to whoever sent it. Update the gem and runruby_native loginagain; an older CLI can no longer complete a sign-in, and the browser says so. - Restoring a purchase now grants the subscription to the account that bought it. One paid subscription could previously be restored onto any number of other accounts.
- A crafted native sign-in link can no longer send someone's session to another app. Only apps using
auth.oauth_pathswere affected.
- A crashed page now recovers instead of freezing or killing the app. When the system reclaims a page's memory, iOS reloads it in place and Android rebuilds the tab; Android previously crashed outright and iOS sat on a blank page until relaunch. Normal Mode; Advanced Mode already recovered on both platforms.
- Background tabs can no longer sign you out, flip the tab bar, or swallow a cold-launch notification tap. Only the tab on screen steers shared state now, matching Advanced Mode. Normal Mode, iOS only.
- Returning to a tab no longer re-fires its last action on Android. A cross-tab link could bounce the tab straight back, and a completed scan could reopen the camera. Normal Mode, Android only.
- The Android splash screen no longer hangs on a first page that fails to load. It gives way after ten seconds, matching iOS.
- Fast navigation can no longer mark the wrong page as a form page, which quietly made the back button skip it for the rest of the session. The same fix keeps
native_presentation_tag :rootfrom clearing history it shouldn't in Inertia apps. Both platforms, Normal Mode. - Saving a form no longer leaves a back button pointing at the submitted form. Landing on a page whose only history is the form behind it now shows no back affordance, and on Android the back press no longer silently does nothing after a navbar-button submit. Both platforms, Normal Mode.
- Purchase buttons no longer risk claiming in-app purchases are unavailable in a build that has them. A script-timing race let the no-IAP fallback claim the button first; it now always yields to the real purchase flow. iOS only.
- Turbo's cached page previews no longer report stale signals. Badge counts and navbar state now always come from the fresh page. Both platforms.
- Share buttons and share menu items now show the Material share glyph on Android by default. Without
icons:they rendered the missing-icon placeholder, since the default is an SF Symbol name. - Tapping a link while a page is still loading no longer shows an error screen. Normal Mode, iOS only.
- The QR code generated by
ruby_native previewnow scans on Android. It failed on dark terminal themes before. - The "Wants to Use ... to Sign In" alert now shows your app's name instead of "RubyNative". iOS only; your next deploy picks it up.
ruby_native deploy --if-neededno longer crashes before an app's first successful build, and--android --if-needednow compares against Android builds instead of iOS ones.
bundle update ruby_native. On its own this closes both sign-in holes above and fixes which account a restore credits.- Run
ruby_native loginagain. An existing CLI session keeps working, but new sign-ins need this version. - Using in-app purchases? Run
bin/rails generate ruby_native:iapand thenbin/rails db:migrate. Youron_subscription_changecallback then fires once per purchase instead of once per restore. Skipping it is safe, and so is re-running the generator. - Recommended: adopt the identity tag. Add
<%= native_identity_tag current_user&.id %>to your layout, outside any signed-in check, and rebuild. Skipping it is safe. Inertia apps: follow the sign out docs.
-
native_presentation_tag :rootlets a page declare that it lands as a root, with nothing behind it and no back affordance, wherever it lands. A screen that is always the first one in its tab — a dashboard reached by scanning a code, the page you land on after choosing an account — now says so about itself, instead of every link and form that reaches it having to say it. Because it is read at the destination rather than at the origin, aPOSTand its redirects carry the intent to wherever the chain actually ends, and a validation failure that re-renders the form declares nothing at all. Not the same asaction: :replace, which swaps the current entry and leaves everything under it. In Advanced Mode it applies before the navigation commits when the page arrives from a form submission, which is the only case where the destination has been fetched by the time the screen is decided; a link tap, a deep link or a cold boot lands first and the stack is corrected once the page renders. Normal Mode applies it on every arrival. Both modes, iOS and Android. Inertia apps get the same thing as aNativePresentationcomponent from@ruby-native/reactand@ruby-native/vue. -
menu.itemacceptsaction: :replaceto control how its page lands. A navbar menu item is a link with native chrome, so it now takes the same push/replace history semantics::replaceswaps the current entry in place, with no back arrow and no stack growth, which is what a page switcher wants. The default stays push. Both Normal and Advanced Mode, iOS and Android. Inertia apps get it as anactionprop onNativeMenuItem. -
navbar.button position: :titleturns the nav-bar title into a dropdown menu. The title becomes a menu button, the native counterpart of SwiftUI'stoolbarTitleMenu: the button'smenuitems switch the current view and the title shows the selected one with a checkmark, defaulting to that item's label. Pair it withaction: :replacefor a page switcher. Both Normal and Advanced Mode, iOS and Android. Inertia apps passposition="title"toNativeButton. -
NativeSegmentbrings navbar segments to Inertia apps. The React and Vue counterpart ofnavbar.segment, with the sametitle,href,click, andselectedprops. iOS only, as segments already were.
- A query string no longer stops a
/newor/editscreen from presenting as a sheet in Advanced Mode. Path configuration patterns are matched against the path and the query string joined together, on both platforms, so the anchored/new$rule missed/orders/new?kind=draftand the screen pushed instead of coming up modally. Nothing about the rule says that, and a filtered or prefilled Rails form is ordinary, so the rules now tolerate a trailing query. Paths that merely contain the word, like/news?page=2, are still pushed. Both platforms. native_tabs_tag enabled: falseno longer throws away the page in Advanced Mode. Any page without a tabs element rebuilt the navigator at the entry point, which is right for the sign-out wall it was written for and wrong for a detail screen that just wants the bar out of the way, so tapping into a record landed the user back on the first tab. The two are now told apart by where the page arrives: a modal restarts, a push hides the bar in place and keeps its stack, and a background tab is ignored. Popping brings the bar back. iOS only.- Signing out of an Advanced Mode app no longer lands on
/. Hiding the tab bar rebuilt the app's single navigator at the bare base URL rather than the entry point a cold launch uses, so an app whose/is a marketing site or a redirect dropped the user there instead of on its first screen. It now starts atapp.entry_path, or the first tab's path when that is unset, which is what launch already did and what Android already does by resetting each tab to its own start URL. iOS only. - Overscroll no longer rubber-bands to white in Advanced Mode. Its web view was left opaque, so WKWebView painted its own white background across the whole scroll extent and covered whatever the page set on
html, including the gap the pull-to-refresh spinner opens.native_overscroll_tagandappearance.background_colornow both reach the overscroll area, as Normal Mode has done since 0.5.0. That is the release that introduced the helper, so it has never worked in Advanced Mode until now. iOS only. - Tab badge counts now reach VoiceOver on iOS. iOS 26 draws the badge but puts nothing in the accessibility tree for it, so the count was invisible to anything not reading pixels. It is published as the tab's accessibility value now, matching what Android already reported. iOS only.
- The Advanced Mode navbar menu button now renders its configured icon, tinted to the navbar's foreground color. It drew a hardcoded, untinted overflow icon that ignored
icon:andicons:, so a branded bar showed no visible glyph; the toolbar's submit button and FAB are themed to match now too. An iOS-only name likeellipsis.circlerenders the missing-icon placeholder on Android instead of a coincidental overflow dot, so give menu buttonsicons: { android: ... }. Android only. - A navbar menu button with no icon falls back to the overflow glyph instead of the missing-icon placeholder. Both Normal and Advanced Mode, Android.
- The Android splash screen shows the customer's icon, not the bundled Ruby Native logo. The per-app icon was written as a mipmap that couldn't override the splash's drawable resource, so a flavor-level
splash_icon.xmlnow points the splash at it. Android only. - A scan on an Android emulator reports
unsupportedinstead of hanging. An emulator declares a camera and grants the permission, but its virtual scene can never resolve a barcode, so the viewfinder opened and sat there with no way for the page to know. It now settles asunsupported, the same outcome as a device with no camera, which reveals your manual-entry fallback. Simulator already did this on iOS. Android only.
icons:now raises when given something other than a hash, instead of silently rendering no icon. Square brackets in ERB produce an array holding one hash, which skipped the per-platform lookup and left the button with no icon on either platform and nothing explaining why. Affectsnavbar.button,menu.item,share_button,share_item, andnative_fab_tag.icons:now works on its own, without a matchingicon:alongside it. A browser has no platform, so nothing matched and the value came back nil, which raised onnative_fab_tagand returned a 500 for a page that rendered fine inside the app. It falls back toicon:and then to any name inicons:, the order the YAML config already uses for tabs.
- Scan barcodes and QR codes with
native_scan_button_tag. Renders a button that opens the native camera scanner; a successful scan fills a target field and fires aruby-native:scanCustomEvent, so the same call works in plain Rails, Turbo, and Inertia. iOS and Android.
window.alert,confirm, andpromptnow show native dialogs on Android. They previously did nothing, andconfirm()/prompt()resolved to their cancelled value, so a page that worked in a browser silently misbehaved. Both Normal and Advanced Mode.- Navbar, FAB, and tab signals now survive navigation in Advanced Mode.
addJavascriptInterfaceonly binds on a real page load, so the bridge went quiet after the first navigation and native chrome stopped updating. Android only. - Links to a path another tab owns now select that tab in Advanced Mode. They pushed onto the current tab's stack instead, because the Hotwire navigator has no tab awareness. Normal Mode already did this. Android only.
- The Advanced Mode FAB renders in the right place and responds to taps. It was added to a root that isn't a
FrameLayout, so its gravity was ignored and it drew top-left underneath the WebView, which is also why taps did nothing. It now survives a back-pop or tab switch too. Android only. - Advanced Mode no longer crashes when a FAB or navbar button uses a relative href. Tapping a control whose
hrefis a path like/books/newkilled the app, because a location is parsed as a full URL before routing; relative hrefs now resolve against the app's start URL, the way deep links already did. Android only. - The Advanced Mode sign-in wall now presents as a modal, matching iOS. Sign-in paths were re-declared as a plain root screen, so sign-up and password-reset stacked a second modal on top instead of pushing inside the existing one, and any auth path that rule didn't name was missed. The whole auth flow is now one modal with internal push navigation. Android only.
- Tapping a non-active tab no longer bounces back to the first tab. In an Inertia or Turbo app the
[data-native-tabs]element briefly leaves the DOM during a client-side swap, which read as a sign-out and reset every tab. Normal Mode, Android. - Detail and form pages no longer reset every tab. Only a completely signal-less page now arms the unauthenticated reset; before, any back navigation or form redirect onto a tabbed page looked like a fresh sign-in. Normal Mode, Android.
- Web content follows system dark mode on Android. The static light theme pinned
prefers-color-schemeto light on every page. Normal Mode. - Pull-to-refresh works again in Normal Mode on Android. Compose's
PullToRefreshBoxis nested-scroll driven and a WebView never dispatches nested scroll, so the WebView now rides in aSwipeRefreshLayout, the same mechanism Advanced Mode uses. - Tab badge counts now reach TalkBack.
NavigationBarItemsilences the badge's own text node, so the count is exposed on the badged item's content description instead. Android only. - A page with an empty
<title>no longer shows its URL in the Advanced Mode toolbar.WebView.getTitle()falls back to the scheme-stripped URL and Hotwire feeds that into the toolbar; iOS already left an empty title empty. Android only. - Advanced Mode no longer crashes on launch when a layout view is missing. Unguarded
findViewByIddereferences threw an uncaught NPE and crash-looped the activity. Android only. - Malformed bridge payloads and hostile config values no longer crash the app. An adversarial unit-test sweep across both packages found and fixed several, including a remote crash vector where the Android signal parser caught only
SerializationExceptionwhile kotlinx's JSON casts throw others. - Screenshot captures report an incomplete run as a failure instead of a success. A run that captured only some paths reported success and quietly delivered a partial set. Captures now retry a path that fails, notice a crashed app in seconds rather than waiting out the full timeout, and say plainly when screenshots are still missing.
- Signing back in no longer leaves the first tab blank under a set title. The auth reset reloaded every tab, including the one that had just rendered the signed-in page; that WebView now becomes the first tab as-is and only sibling tabs reload, matching iOS. Normal Mode, Android.
- Standalone navbar buttons now render in Advanced Mode. Only dropdown menus and the submit button made it into the toolbar, so a plain icon button like the barcode scan button was silently dropped. Android only.
- The FAB's shadow fades out instead of cutting off in a hard line. The shadow drew past its host view's bounds and clipped; the button's standoff now leaves room for it. Advanced Mode, Android.
- The Android config error screen now matches iOS. Retry re-fetches the config, the demo app adds a Change servers escape hatch and a View error details dialog with gem and app versions and copyable output, and a customer's shipped app shows only generic end-user copy instead of raw config detail.
- The Android demo app shows the Ruby Native version, not its own build number. The welcome screen displayed the internal versionName; it now shows the library version like iOS, and the launcher shortcut says "Switch website" to match.
- An icon name that doesn't resolve now shows a placeholder instead of a blank button on iOS. A name that isn't an SF Symbol on the running device rendered nothing at all, and a button with no title disappeared entirely, so a typo could silently delete a working action. Android already fell back to a visible glyph. iOS only.
- The missing-icon placeholder reads more clearly as a broken icon on Android. A plain question mark in a tab or nav bar looks like a deliberate help affordance; it's now a bracketed question mark, matching the intent of the iOS placeholder. Android only.
- Advanced Mode now opens every OAuth sign-in in the native sign-in session. A provider link rendered as a
button_towithdata-turbo="false"(the usual OmniAuth shape, since it only accepts POST) or reached through a server-side redirect loaded inside the web view instead ofASWebAuthenticationSession. This also unblocks providers that check for a managed device, like Microsoft Entra Conditional Access, because Microsoft's SSO plug-in only attaches to the native session. Normal Mode already handled both. iOS only. - An OAuth path that sits under a tab's path now signs in instead of switching tabs in Advanced Mode. Tab routing matched first and swallowed the sign-in. iOS only.
@ruby-native/reactand@ruby-native/vuenow ship TypeScript declarations. Importing a component gives you autocomplete and prop checking with no@typespackage to install;NativeIcons,NativeButtonPosition, andNativeHapticFeedbackare exported as types. The declarations are generated from the package source at publish, so they can't drift from the components.- Pop the navigation stack from React and Vue with
NativeBackButton. The counterpart ofnative_back_button_tag: renders a chevron unless given children, and forwards extra props to the underlying button. - Check the current platform from React and Vue with
nativePlatform(). Returns"ios","android", ornullon the web, matching thenative_platformRails helper. - File links now open in-app in QuickLook instead of ejecting to a logged-out browser. A PDF, image, or other attachment opened by a link tap or
window.openused to open a session-less browser showing a login screen; it now downloads with the app's session and previews in QuickLook. Covers link taps andwindow.openin Normal Mode andwindow.openin Advanced Mode. iOS only.
- Normal Mode no longer crashes on popups that open a non-web URL. A
window.opentargeting amailto:,tel:, or auth-provider scheme (like a Sign in with Apple popup) crashed the app because the in-app browser rejects non-http(s) URLs; these now hand off to the system handler while web URLs still open in the in-app browser. iOS only.
- Configure how push finds the signed-in user with
RubyNative.current_user_resolver. Defaults tocurrent_user; set it to a method name or a callable like-> { Current.person }so apps that expose the current user another way don't need acurrent_useralias just for push.
- Inline navbars now read with
<%=in ERB. Thenavbarandmenubuilder methods return a blank string, so<%= navbar.button %>renders nothing extra and passeserb_lint, which flags a bare<%call as an unused expression. Existing<%usage still works.
- Microphone capture now actually starts on Android.
getUserMediacleared the permission but failed withNotReadableErrorbecause the app didn't declareMODIFY_AUDIO_SETTINGS, which Chromium's WebView needs to open the audio device. Apps that opt into the microphone now declare it. Applies to both Normal and Advanced Mode. - The file-input camera option now works in Advanced Mode on Android. Tapping Camera in a file picker silently did nothing because the app declared
CAMERAbut never requested it at runtime; the picker now requests the camera permission first, matching thegetUserMediaflow.
- Add segments to the navigation bar with
navbar.segment. Render up to a few segmented buttons in the bar to switch between closely related pages (iOS only for now); mark the current page's segmentselected. Switching segments replaces history instead of stacking it, so the back button doesn't step through the switches.
- Microphone and camera capture now work in Advanced Mode on Android. Web
getUserMediarequests were silently denied with no permission prompt; they now request the native runtime permission and grant the web view, matching Normal Mode and iOS. native_versionnow reports the real version in Android apps. The Android User-Agent hardcoded the version, so the helper always returned0.1.0; it now reflects the installed build, matching iOS.- Advanced Mode tab bar no longer has extra padding below it on Android. The gesture inset was applied twice, leaving an empty band beneath the tab labels; it now sits flush against the gesture bar, matching Normal Mode.
- Add a launch splash screen on iOS with
appearance.splash. Setenabled: trueto show your launch icon andbackground_colorwith an activity indicator until the first screen loads, instead of flashing to a blank web view.
- Brand the navigation bar with
appearance.navbar. Set a centeredlogo, barbackground_color/foreground_color, and astatus_barstyle (light/dark) to brand the bar across the whole app; the logo replaces the page title on every screen. config/ruby_native.ymlis now evaluated as ERB. Interpolate Rails helpers into your config, most usefullylogo: "<%= image_url('logo.png') %>"so the navbar logo points at a fingerprinted asset the app downloads once and caches.
- Advanced Mode tab bar icons now render on Android. Configured Material Symbols (
icons.android) previously fell back to a blank placeholder for all but a few names; each tab now draws its icon, matching Normal Mode and iOS.
- Tab labels now always show in Advanced Mode on Android. With four or more tabs, Android hid the label on unselected tabs; every tab now shows its title, matching iOS and Normal Mode.
navbar.share_buttonadds a native share button to the nav bar. Tapping it opens the iOS share sheet for the current page, or a customurl:.
- Muted background videos no longer force fullscreen on iOS. Web views now enable
allowsInlineMediaPlayback, soplaysinlinevideo plays inline in both Normal and Advanced Mode. [data-native-app]is now reliably set on Android in Normal Mode. The marker applies at document start, matching iOS, so CSS keyed to it (like.native-hiddenand the safe-area inset fallbacks) takes effect.- Microphone and camera capture (
getUserMedia) now works on Android. Media requests bridge to a native runtime permission instead of being silently denied, matching iOS; opt in per app with the usage descriptions in your app settings.
- The native error and offline screens are now customizable and localized. Set per-state icons (
offlinefor no connectivity,genericfor any other load failure) inconfig/ruby_native.ymlundererrors:, using the sameicon:/icons:form as tabs, and put title, message, and shared retry copy in your app's own locale files under theruby_native.errors.*namespace.
- The floating action button and native nav bar now render in apps without a tab bar. When
config/ruby_native.ymlhad notabs:section, the iOS bridge only reported native signals on pages that declared a tab bar, so a tab-less app's FAB and nav bar never appeared. The bridge now reports whenever any native signal is present (FAB, nav bar, form, or push), and the Normal Mode FAB anchors to the bottom safe area when there is no tab bar to sit above. - A presented modal no longer removes the Advanced Mode FAB. Opening a modal (a
/newor/editscreen) whose page declared no FAB tore down the underlying tab's FAB, and dismissing the modal did not bring it back.
- Relaxed the
jwtdependency to>= 2.0, < 4so apps on jwt 3 (for example, anything using Intercom's JWT helper) can install the gem. The previous~> 2.0pin forced a resolution conflict. The Apple IAP webhook decoder now passesalgorithms: ["ES256"], which behaves identically on jwt 2 and 3.
- The native tab bar no longer resets to the first tab during SPA navigation. Inertia and Turbo briefly drop the
data-native-tabselement on each page change; the iOS Normal Mode runtime now debounces the signal to ignore the transient drop. - The floating action button (
native_fab_tag) now follows the visible tab. In tab-based apps it could vanish when you returned to its tab and then stick on every tab after a pull-to-refresh, in both Advanced and Normal Mode. - The Advanced Mode navbar submit button keeps its enabled state after a form submit in another tab. A background tab reloading could repoint the
navbar.submit_buttondisabled toggle and leave the visible form's button stuck.
- Advanced Mode navbar buttons now appear on the first paint after a modal dismisses. When a modal's form redirected to a new screen, the destination's buttons were dropped until a manual pull-to-refresh because the view controller wasn't on screen when the signal arrived. The navbar now retries briefly until the screen settles.
- OAuth callback paths in
auth.oauth_pathsno longer trip a native sign-in loop. Listing a provider's callback next to its authorize path made the iOS app treat the callback as a sign-in entry point and loop. The gem now drops any callback that duplicates a listed authorize path and warns; list only authorize paths.
- The config error screen now flags gem/native version mismatches and shows the fix: the
bundle update ruby_nativecommand for an old gem, or an Update app button for a stale app. When versions match, it names the offendingconfig/ruby_native.ymlkey.
- Config errors no longer leak developer detail to end users. Shipped apps show a generic message; the Preview app adds a Change servers button and shows the unreachable server URL.
- Tabs that use only
icons:now keep working on older native binaries. When a tab defines the per-platformicons: { ios:, android: }form without a flaticon:, the served config now also populates the legacyiconfield fromicons.ios(falling back toicons.android). Native binaries that predate theicons:option read onlytab.icon, so without this they showed no tab icon. An expliciticon:is always preserved.
- Pull-to-refresh on pages with a native navbar. Any page that renders
native_navbar_tagnow reloads when the user pulls down from the top, on both iOS and Android Normal Mode. The reload uses the current URL, which means Turbo and Inertia pages refresh correctly without any extra work. Passpull_to_refresh: falsetonative_navbar_tagon a specific page to opt out. Advanced Mode already supported pull-to-refresh through Hotwire Native, so no change is needed there. - Linked domains for iOS. Tapping a link to your site now opens the app instead of Safari, and saved passwords from your site autofill in the app's web views. Opt-in by adding
ios.bundle_idandios.team_idtoconfig/ruby_native.yml; the gem then serves/.well-known/apple-app-site-associationautomatically and the build pipeline includes the entitlement in every iOS build. See Linked domains.
- Install generator template revamped with a uniform documentation style. Every field now has a block comment above it describing what it does, listing the options with their meanings, and noting the default. Existing apps are unaffected; only
rails generate ruby_native:installoutput changes.
app.namedefault.RubyNative.config[:app][:name]no longer falls back to"Ruby Native"when missing from the YAML — the value was never read anywhere. If you somehow relied on the default, set the key explicitly.
ruby_native preview --url URLpoints the cloudflared tunnel at an arbitrary upstream instead ofhttp://localhost:PORT. Useful when your Rails app runs behind a docker compose entry point that isn't directly reachable from a phone.
- In-app review prompts. New
native_review_taghelper (andNativeReviewcomponent for React and Vue) asks iOS to show the App Store rating prompt when the page loads. Apple throttles when it actually appears, so it is safe to render anywhere; it is also suppressed during screenshot runs.
icons:option for per-platform icon names. Passicons: { ios:, android: }alongside the existingicon:string on navbar buttons, menu items, FABs, and tabs to use platform-specific icon identifiers. Resolves via the native platform parsed from the Ruby Native UA, falling back toicon:when the platform-specific value is missing. Available in the ERB helpers, the React package, and the Vue package. Motivated by Android needing Material Symbols names (e.g.coffee,shopping_bag) instead of the SF Symbols identifiers (cup.and.saucer,bag) that work on iOS. Additive: existing configs usingicon:keep working unchanged.ruby_native deploy --androidtriggers an Android cloud build instead of iOS. Polling and success messaging adapt to Play Internal Testing. Pass--platform=ios|android|allfor explicit control; default behavior is unchanged.
- Rails 8.1 compatibility for the gem's middleware initializers. Rails 8.1 replaced the initializer array with a sorted graph, which could place
ruby_native.oauth_middlewareandruby_native.tunnel_cookie_middlewareafter the middleware stack was already frozen. That broke the host app entirely, includingbin/rails middlewareand every generator. Both initializers now runbefore: :build_middleware_stackso they are ordered correctly. No config changes needed.
screenshot_sign_inlambda now receives a helper object instead of the raw controller. In Rails 8.1,cookiesis private onActionController::Base, so the previouscontroller.cookies.signed.permanent[...] = ...pattern raisedNoMethodErrorand/native/screenshots/sessionreturned 500. The lambda is now yielded a small helper exposing publiccookies,request, andsessionaccessors. Breaking-but-allowed: update your initializer's lambda parameter to a name likehelperand callhelper.cookies/helper.request/helper.sessioninstead ofcontroller.cookiesetc. The 0.9.0 API had no production users to migrate.
- Authenticated App Store screenshots from a real iOS Simulator. Generate a per-app screenshot key on the Ruby Native dashboard, register a
screenshot_sign_inlambda in your initializer, and Ruby Native captures App Store screenshots against your deployed site, signed in as a designated user. RubyNative.configureblock. Setc.screenshot_keyandc.screenshot_sign_infor the screenshot session endpoint.GET /native/screenshots/sessionendpoint mounted by the gem engine. Validates the screenshot key, calls the configured sign-in lambda, and sets a session-scoped cookie.ruby_native_screenshot_session?view helper. Returns true when the current request is part of a screenshot run, so views can render deterministically (frozen timestamps, hidden push banners, suppressed analytics).
ruby_native screenshotsCLI command. Screenshots are now captured by Ruby Native's CI infrastructure, not by a local Playwright run. Calling the old command after upgrading prints a deprecation message and exits non-zero.
native_fab_tagfloating action button signal. Renders a native floating button above the tab bar. Acceptsicon:(SF Symbols name, required),href:(URL to visit), andclick:(CSS selector to click). Shows/hides per page based on signal presence. Works in both Normal and Advanced Mode. iOS 26+ renders with Liquid Glass styling, older versions use a bordered button.NativeFabcomponent for React and Vue. Same API as the ERB helper:icon,href, andclickprops.
TunnelCookieMiddlewareno longer joins multi-cookie responses with"\n". Under Rack 3 this caused browsers to drop every cookie after the first, breaking login through the Cloudflare tunnel preview. Multi-cookie responses now return anArrayofSet-Cookiestrings, matching Rack 3's contract.
- Tab routing no longer cancels navigation from an unclaimed URL back to a tab's claimed URL in Normal Mode. Under Turbo this could lock up the tab bar; under Inertia the page silently failed to navigate. See #50 for details.
ruby_native deploy --if-neededskips the build when the gem version matches the last successful build. Designed for CI: add it as a post-deploy step and it's a no-op until you bump the gem. Triggers the build and exits immediately without polling.RUBY_NATIVE_TOKENenvironment variable for CLI authentication in CI. Takes priority over the stored credentials file. Generate a token withruby_native loginlocally, then set it as a CI secret.- Build failure email notifications. When a build fails, the account owner receives an email with the error details and a link to the builds page.
- Tab auto-routing with trailing-slash patterns no longer breaks Normal Mode navigation. Routes like
/breweries/now correctly match the bare path/breweries, preventing an infinite routing loop when Turbo.js is present. - Tab path matching now groups routes by tab index.
_tabPathsis an array of arrays instead of a flat array, so multiple auto-route patterns on the same tab are correctly identified as the same tab.
-
Add
viewport-fit=coverto your viewport meta tag. The native bridge no longer injects it automatically. Inject-after-the-fact was unreliable when the page already had a viewport meta tag, because WebKit can resolve safe area insets before the JS runs, leavingenv(safe-area-inset-*)at0and breakingnative-inset. Update your layout:<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
- Advanced Mode no longer uses bridge components. The
@hotwired/hotwire-native-bridgeJavaScript dependency, theruby_native/bridgeStimulus controllers, and the per-feature SwiftBridgeComponentclasses are all removed. Advanced Mode now reads signal elements (data-native-*) via the sameRubyNative.jsbridge that Normal Mode already uses. One code path for both modes, significantly less surface area to maintain. - Removed deprecated helpers that only emitted the bridge Stimulus markup. If you were still calling any of these, migrate to the signal-based equivalents:
native_button_tag,native_menu_tag→ usenative_navbar_tagwith nestednavbar.button(...)andnavbar.button do |button| button.item(...) end.native_form_data,native_submit_data→ usenative_form_tagplusnavbar.submit_buttoninside anative_navbar_tag.native_search_tag→ no signal-based replacement yet; reach out if you need it.
- Helpers now emit only signal markup.
native_tabs_tag,native_push_tag,native_badge_tag, andnative_haptic_datapreviously emitted bothdata-native-*and the redundantdata-controller="bridge--*"attributes. They no longer emit the bridge half, which removes some DOM noise but means any code that relied on observing those Stimulus controllers will stop firing. navbar.buttontakes title as its first positional argument. The oldtitle:kwarg form no longer works. Update nested navbar buttons fromnavbar.button title: "Sign out", icon: "..."tonavbar.button "Sign out", icon: "...". Applies to both the ERB helper and the equivalent usage in React/Vue (which already used positional-style props).
native_navbar_tagnow accepts no title. Callingnative_navbar_tag do |navbar| navbar.button(...) endwithout a title string is valid. In Advanced Mode, the destination keeps whatever title Hotwire Native derived from the HTML<title>tag. When a title IS provided, the sharedRubyNative.jsbridge also syncsdocument.titleso Hotwire's own title observer can't race into the wrong value.- Bar buttons with both title and image keep the title for accessibility. If you set both
title:andicon:on anavbar.button, the native bar button shows the SF Symbol visually and uses the title as the VoiceOver label automatically. The Swift side builds these viaUIBarButtonItem(title:image:primaryAction:menu:)so UIKit handles the accessibility wiring. @ruby-native/reactand@ruby-native/vueNativeNavbartitleprop is optional.<NativeNavbar>{children}</NativeNavbar>works without a title for Inertia apps, mirroring the ERB helper's behavior.
- Remove
@hotwired/hotwire-native-bridgefrom your JavaScript dependencies and delete theimport "ruby_native/bridge"line from your entrypoint. - If you have Advanced Mode pages still using
native_button_tag,native_menu_tag,native_form_data, ornative_submit_data, migrate them tonative_navbar_tagwith nested signals (see the Advanced Mode guide). - Update any
navbar.button title: "..."call sites to pass the title positionally:navbar.button "...", icon: "...". - Rebuild your Ruby Native iOS app against gem 0.7.0 (or use the cloud build pipeline to regenerate it).
- Scoped npm packages. The unscoped
ruby-nativenpm package is replaced with two scoped packages published under the Ruby Native org:@ruby-native/reactand@ruby-native/vue. Update your imports:import { NativeTabs } from "ruby-native/react"→import { NativeTabs } from "@ruby-native/react"import { NativeTabs } from "ruby-native/vue"→import { NativeTabs } from "@ruby-native/vue"- In
package.json, replace"ruby-native": "..."with"@ruby-native/react": "^0.6.0"or"@ruby-native/vue": "^0.6.0"depending on your framework.
- React and Vue Inertia components now build cleanly in Vite consumers. The
@inertiajs/*router is imported dynamically (silently skipped when Inertia isn't installed), andreact/vueare no longer marked optional inpeerDependenciesMeta(only@inertiajs/reactand@inertiajs/vue3remain optional). Previously, Vite's optional-peer-dep shim broke the components' static imports.
auto_routetab config option for controlling tab routing. Acceptsfalse(disable routing for this tab) or an array of route prefixes (replaces the default prefix match onpath). Trailing slash routes like/breweries/match sub-paths but not the bare path. Omitauto_routeto keep the default behavior (prefix match on the tab'spath).
eager: truetab config example to the install generator's YAML template. Seteager: trueon a tab to load it on app launch instead of when first tapped.
- Apple webhook endpoint now handles
TESTnotification type. Previously, test notifications sent via the App Store Server API's "Request a Test Notification" endpoint would return HTTP 500 because the processor tried to decode asignedTransactionInfothat doesn't exist in test payloads. The endpoint now returns 200 immediately forTESTnotifications without processing.
- Always edge-to-edge. The
edge_to_edgeconfig option has been removed. The web view always extends behind the status bar and Dynamic Island. Add thenative-insetCSS class to your content wrapper to handle safe area spacing (see below). status_bar_colorrenamed tobackground_color. The oldbackground_color(unused at runtime) has been removed. The newbackground_coloris the unified window background color, visible during app launch and transitions. Set it to match your CSS body background or omit it.viewport-fit=coverinjected automatically. The JavaScript bridge now injectsviewport-fit=coverinto the viewport meta tag at document start. This enables CSSenv(safe-area-inset-*)variables. No manual viewport changes needed.
native-inset,native-inset-top, andnative-inset-bottomCSS utility classes in the gem stylesheet. These use::before/::afterpseudo-elements so they stack with existing padding utilities likepb-8. Include the stylesheet withstylesheet_link_tag :ruby_native.native_overscroll_tagERB helper for per-page overscroll colors. Declares top and bottom colors that are dynamically swapped based on scroll position, solving the WKWebView limitation of a singlebackground-colorfor all overscroll directions. Usage:native_overscroll_tag(top: "#f0f9ff", bottom: "#f5f1ea").NativeOverscrollcomponent for React and Vue Inertia apps. Usage:<NativeOverscroll top="#f0f9ff" bottom="#f5f1ea" />.- Overscroll color logic in the bundled JavaScript. Detects
data-native-overscroll-topanddata-native-overscroll-bottomsignal elements via the MutationObserver and swapshtmlbackground-color based on scroll position. Works with Turbo and Inertia navigation.
- Update
config/ruby_native.yml: removeedge_to_edgeandstatus_bar_color. Rename your oldstatus_bar_colorvalue tobackground_color(or remove the oldbackground_colorif it was the same). - Add
<%= stylesheet_link_tag :ruby_native %>to your layout<head>if not already present. - Add the
native-insetclass to your main content wrapper:<main class="native-inset">. - For fixed navbars, add
native-inset-topto the<nav>element.
appearance.landscapeconfig option. Setlandscape: trueinconfig/ruby_native.ymlto allow landscape orientation on iPhone. Defaults to portrait only.
enabledprop onNativeTabsfor React and Vue Inertia components. Passenabled={false}(React) or:enabled="false"(Vue) to dynamically hide the tab bar, for example during edit mode. Defaults totrue.
-
RubyNative::InertiaSupportshared props renamed fromnative_app/native_formtonativeApp/nativeFormto match JavaScript naming conventions. Update your Inertia components to use the new camelCase names. -
Vite + Inertia apps: The gem's React and Vue entry points now import from
@inertiajs/reactand@inertiajs/vue3respectively. If you resolve the gem's JavaScript via a Vite path alias, add the Inertia package toresolve.dedupein yourvite.config.tsto prevent Vite from resolving it relative to the gem's bundler path instead of your app'snode_modules:// React dedupe: ["react", "react-dom", "@inertiajs/react"], // Vue dedupe: ["vue", "@inertiajs/vue3"],
native_navbar_tagERB helper for rendering a native navigation bar in Normal Mode. Supports trailing/leading buttons with icons,hrefnavigation,clickelement targeting, and nested menu items via a builder API. Includessubmit_buttonfor native form submission.NativeNavbar,NativeButton,NativeMenuItem, andNativeSubmitButtoncomponents for React (ruby_native/react) and Vue (ruby_native/vue) Inertia apps. Render signal elements that the native bridge parses into a native navigation bar with buttons, menus, and submit actions.native-hiddenCSS class. Elements with this class are hidden when running inside the native app. Use for web UI that has a native equivalent (e.g., buttons targeted byclick). Requires the gem stylesheet (stylesheet_link_tag :ruby_native).ruby_native deployCLI command triggers an iOS build from the terminal, polls for status, and reports success or failure. Links to your app on first run and saves the selection. Blocks concurrent deploys when a build is already in progress.
native_badge_taghelper for updating the app icon badge and tab bar badge from page loads. Works in both Normal Mode (viadata-native-badgesignal element) and Advanced Mode (viabridge--badgeStimulus controller). Pass a single count to set both badges, or usehome:andtab:keyword arguments for independent control. Omitted parameters leave that badge unchanged.RubyNative.setBadge(5)JavaScript API for updating badges programmatically. Accepts a number (sets both) or an object withhomeandtabkeys.badge: truetab config option inconfig/ruby_native.ymlto designate which tab receives the tab bar badge.
appearance.themeconfig option to force light or dark mode. Acceptslight,dark, orauto(default). When omitted, follows the device setting.
ruby_native loginandruby_native logoutCLI commands for authenticating with the Ruby Native platform via browser-based OAuth flow.ruby_native screenshotsCLI command captures web screenshots via Playwright, then uploads them to the platform for compositor processing. Prompts for Playwright install on first run. Auto-links to an app on your account and persists the selection inconfig/ruby_native.yml. Pass--urlto capture from a different host (e.g., a production or staging URL).- Install generator adds
.ruby_native/to.gitignore.
- Preview QR code no longer garbles the first line of output in process managers like Overmind. Replaced the terminal-clear escape sequence with simple newline spacing.
import "ruby_native/bridge"now resolves correctly for importmap-rails apps. Previously, the barrel import was pinned asruby_native/bridge/indexwhich required users to manually add the pin. The gem's importmap config now explicitly pinsruby_native/bridgeto the index file.- Install generator now prints Advanced Mode setup instructions (pinning
@hotwired/hotwire-native-bridgeand adding the import) so users aren't left guessing.
native_haptic_datahelper for triggering device haptics on tap. Works in both Normal Mode (viadata-native-hapticattribute) and Advanced Mode (viabridge--hapticStimulus controller). Accepts a feedback type::success(default),:warning,:error,:impact, or:selection. Unknown and blank values default to:success.RubyNative.haptic("success")JavaScript API for triggering haptics programmatically.RubyNative.postMessage()JavaScript API that wraps the native message handler. All internal JS-to-native communication now routes through this method.
native_form_datanow accepts**datakeyword arguments and merges thecontrollerkey instead of clobbering it. Existing usage without arguments is unchanged.native_back_button_tagonclick now usesRubyNative.postMessage()instead of callingwebkit.messageHandlersdirectly.
tabsis now optional inconfig/ruby_native.yml. Omit it to show a single full-screen web view without a tab bar. The app loadsentry_pathor falls back to/.- Config endpoint returns an
X-Ruby-Native-Versionresponse header with the gem version. - Tapping the version number on the error screen opens a detail sheet with the full error message, app version, gem version, and a copy-to-clipboard button.
- Tab bar is only shown when two or more tabs are configured. A single tab no longer renders an empty-looking tab bar.
entry_pathnow defaults to the first tab's path (then/) instead of always falling back to/.
appearance.edge_to_edgeconfig option to let the web view extend behind the status bar and Dynamic Island. Enables full-bleed backgrounds, gradients, and images. Normal Mode only.
- Session cookies no longer silently fail through the Cloudflare preview tunnel. Apps that configure
domain: :allon their session store had cookies scoped to.trycloudflare.com(a public suffix), which browsers and WKWebView reject. A new middleware automatically strips thedomainattribute from cookies on tunnel requests so they scope to the exact hostname instead.
app.entry_pathconfig option to control the initial URL on app launch. Defaults to the first tab's path, then/. Removes the need for server-side redirects to route native users to the right page.
- OAuth middleware no longer relaxes
SameSiteon session cookies for non-native requests. Previously, all requests to configuredauth.oauth_pathshad their cookies changed toSameSite=None; Secure, which silently dropped session cookies in non-HTTPS environments like Rails integration tests.
- Inertia.js support for React and Vue apps. Include
RubyNative::InertiaSupportin your application controller to sharenative_appandnative_formprops automatically. - React signal components (
NativeTabs,NativePush,NativeForm) importable fromruby_native/react. - Vue signal components (
NativeTabs,NativePush,NativeForm) importable fromruby_native/vue.
- Removed importmap pins for
ruby_native/reactandruby_native/vue. Inertia apps resolve these via the Vite alias instead. RubyNative::InertiaSupportraises a clear error if theinertia_railsgem is not installed.
- OAuth redirect loop when Devise (or similar) stores the OAuth start page as the post-login redirect. The middleware now replaces
/native/auth/start/*redirect URLs with/before building the token.
- OAuth start page now uses the path from
auth.oauth_pathsin config instead of hardcoding/auth/{provider}. Fixes 404 errors for apps that mount OmniAuth at a custom prefix like/users/auth/.
- User agent now includes
RubyNative/X.Y.Zpackage version token.native_versionreturns the package version instead of the app version, making it consistent across all apps built with Ruby Native. native_versionreturns"0"for older apps that don't include theRubyNative/token.
- OAuth support for native apps. Add
auth.oauth_pathstoconfig/ruby_native.ymlto enable Sign in with Google, Apple, and other providers. native_versionhelper for version-gating features in views, e.g.native_version >= "1.4".
- Config endpoint now always includes
app.namein JSON response, defaulting to "Ruby Native". Fixes compatibility with older versions of the Preview app.
- Added default chevron icon SVG to web-based back button
- Advanced Mode with native navigation bar, screen transitions, and swipe-to-go-back.
- Six bridge controllers (
tabs,form,button,push,menu,search) registered viaimport "ruby_native/bridge". native_form_dataandnative_submit_datahelpers for native submit buttons that disable during form submission.native_button_taghelper for native navigation bar buttons with SF Symbol icons and left/right placement.native_menu_taghelper for native action sheet menus with destructive item support.native_search_taghelper for native search bars that dispatch query events.- Bridge component CSS in the gem's stylesheet. Web submit buttons and native button elements are automatically hidden when their bridge components are active.
- Fix potential issue with rendered QR code
- Generator copies CLAUDE.md with
copy_fileinstead oftemplateto avoid ERB evaluation errors. - Removed erroneous
.jsonextension from config endpoint path in generated documentation.
No functional changes. Version bump only.
rails generate ruby_native:installgenerator.- Generator creates
config/ruby_native.ymlwith sensible defaults. - Generator adds
.trycloudflare.comto allowed hosts indevelopment.rb. - Generator copies
.claude/ruby_native.mdif a.claude/directory exists. - Config controller reloads config on each request in development.
- Dark mode documentation (light/dark color objects).
- Improved CLI usage output with list of available commands.
- Updated README with generator-based getting started flow.
bundle exec ruby_native previewCLI command with Cloudflare tunnel and QR code.--portoption for specifying local server port (defaults to 3000).rqrcodegem dependency.- MIT LICENSE.
- Rails engine auto-mounted at
/native. GET /native/configendpoint returning YAML config as JSON.POST /native/push/devicesendpoint for push token registration.native_app?helper for detecting native app requests.native_tabs_tag,native_form_tag,native_push_tagsignal element helpers.ruby_native.cssstylesheet with native back button support.RubyNative::NativeDetectioncontroller concern.