Screenshots

Both Apple and Google require screenshots before you can publish a production listing. They're often the first thing a potential user sees in store search results.

#How Ruby Native captures screenshots

When you click Capture on the Screenshots page, Ruby Native boots a real device on its CI infrastructure (an iOS Simulator for iOS, an Android emulator for Android), launches your app, signs in as a designated user, navigates each path you've configured, and captures a screenshot at the native resolution of the device.

Each capture takes about 5 minutes for iOS and 5 to 10 minutes for Android (the emulator-cold-boot step is the bulk of the difference). The resulting PNGs land back on the dashboard.

Setup is the same for both platforms. Once you've configured the key, the sign-in lambda, and the paths, you can capture for either platform with one click each.

#Setup

Open the Screenshots page in your Ruby Native dashboard and follow the three-step wizard. The summary:

#1. Generate the key

Click Generate key. The plaintext is shown once. Copy it immediately, Ruby Native stores only an encrypted copy and won't display it again. If you lose it, regenerate it (which invalidates the prior one).

#2. Configure your Rails app

Add the key to your Rails credentials:

bin/rails credentials:edit
ruby_native:
  screenshot_key: <paste the key>

In config/initializers/ruby_native.rb, register the screenshot key and a sign-in lambda. The same lambda handles both iOS and Android, the bridge hits the same /native/screenshots/session endpoint from either platform.

Your lambda needs to establish a session the same way a real login does. The simplest rule: do whatever your SessionsController#create does after the password checks out. If your auth just sets a cookie, set the cookie. If it creates a session record, create the record too. Setting a cookie alone isn't always enough.

helper exposes cookies, request, and session as public accessors. Methods your ApplicationController mixes in (like Devise's sign_in helper) aren't available, so manipulate the cookie jar, session, or warden proxy directly.

Cookie-based auth. Devise, Clearance, or a hand-rolled encrypted cookie. Signing in is one line:

RubyNative.configure do |c|
  c.screenshot_key = Rails.application.credentials.ruby_native.screenshot_key
  c.screenshot_sign_in = ->(helper) {
    user = User.find_by!(email: "[email protected]")
    # Pick the line that matches your auth library:
    #   Devise:    helper.request.env["warden"].set_user(user)
    #   Clearance: helper.cookies[:remember_token] = user.remember_token
    #   Custom:    helper.cookies.permanent.encrypted[:user_id] = user.id
  }
end

Session-record auth. Rails 8's built-in authentication generator and most roll-your-own systems authenticate by looking up a Session record from the cookie. A cookie alone won't sign you in here, the record it points to has to exist. Create it, then set the cookie:

RubyNative.configure do |c|
  c.screenshot_key = Rails.application.credentials.ruby_native.screenshot_key
  c.screenshot_sign_in = ->(helper) {
    user = User.find_by!(email: "[email protected]")
    session = user.sessions.create!(
      user_agent: helper.request.user_agent,
      ip_address: helper.request.remote_ip
    )
    helper.cookies.signed.permanent[:session_id] = {
      value: session.id, httponly: true, same_site: :lax
    }
  }
end

#3. Pick paths to capture

In the dashboard, list the paths you want to screenshot, one per line:

/dashboard
/projects
/settings

The same paths are used for both platforms. Each store has its own minimum:

Store Minimum Maximum Capture resolution
App Store 3 10 1320 × 2868 (6.9" iPhone)
Play Store 2 8 1080 × 2400 (Pixel 6)

Pick at least 3 to satisfy both stores.

#Tips for good screenshots

  • Designate a screenshot user with realistic data. Empty states make a bad first impression. Seed the user with several projects, completed onboarding, etc.
  • Use the screenshot session cookie for determinism. Wrap relative timestamps, push banners, A/B variants, and analytics calls in if ruby_native_screenshot_session? so every capture renders identically.
  • Lead with your best screen. The first screenshot appears in store search results. Pick the one that best communicates what your app does.
  • Keep it focused. A few great screenshots that tell a story are better than 10 mediocre ones.

#Risks and mitigations

The endpoint at /native/screenshots/session is permanent and hittable from anywhere. The whole security model rests on three things:

  1. The screenshot key is in your Rails credentials, not in source. Without the key, every request to the endpoint gets a 401. The key is sent via URL parameter on the device's first navigation; the gem auto-filters it from Rails.application.config.filter_parameters, so it won't appear in your access logs as anything other than [FILTERED].

  2. You designate which user gets signed in. Use a sandboxed account, not an admin. The sign-in lambda runs in your Rails process; the gem can't see or modify it. If an attacker gets the key, the worst they can do is sign in as the screenshot user.

  3. Rotation is one click. "Regenerate" on the dashboard invalidates the old key. Update your Rails credentials and redeploy and the leaked key is dead.

What this is not: a tunneled, ephemeral, or short-lived auth flow. It's a long-lived shared secret. If you need stricter control (audit log, per-capture token, IP allowlist on the endpoint), open a support ticket and we'll discuss. Most apps don't need it.

#Troubleshooting

Capture finishes but no images uploaded. The device timed out trying to authenticate. Check the workflow run logs, usually the auth path returned 401 because the dashboard's stored key doesn't match what's in your Rails credentials. Regenerate from the dashboard, update credentials, redeploy.

Pages render but show empty / signed-out content. Two common causes. Either your screenshot_sign_in lambda signed in a real user who happens to have no data (add seed data or designate a different user), or the lambda set a cookie but your auth resolves the current user from a session record it never created. For the second case, mirror your real SessionsController#create: create the record, then set the cookie to point at it. See the session-record example in setup.

Pages flash with skeleton loaders or missing content (iOS). The bridge fires the "ready" signal as soon as page chrome enumerates, but customer-side hydration (Stimulus, Inertia, XHR) may continue after that. Wrap deterministic hooks in ruby_native_screenshot_session? or contact support if your app needs an explicit RubyNative.markReady() JS hook.

Capture hangs partway through (Android). The emulator may have hit a system UI ANR or wedged on a path that never fires the ready signal. The capture run is time-boxed per path, so a hang on one path skips the rest. Try removing or splitting the offending path.

#Next steps

Once your screenshots are captured, fill out the store's data declarations before submitting: