MyVitals
Engineering

I Shipped an App Store App That Updates Itself, Because There's Nothing in It to Update

How MyVitals reaches the Play Store with almost no native UI — a thin WebView shell pointed at a live URL — and the pile of small native tricks it takes to make a website stop feeling like one.

S

Suyash Vashishtha

3 September 2026 · 3 min read

ShareWLF
I Shipped an App Store App That Updates Itself, Because There's Nothing in It to Update

com.myvitals.app, installed from the Play Store, icon and splash screen and everything, has almost no native UI. Open the source: a splash screen, an error screen, one WebView pointed at https://myvitals.co.in. That's the app.

export const WEBVIEW_URL = 'https://myvitals.co.in';

That string is functionally the entire product. Ship a fix to the web frontend — a normal deploy — and every installed app gets it next open. No review queue, no staged rollout, no "update available" nag. The native shell itself changes maybe a few times a year (a permission, an icon); everything users experience as "the app" ships at web speed, to 100% of installs, at once.

The trade

  1. No true offline bundle — if the WebView can't reach the internet on first paint, there's nothing local to fall back to. (The actual offline story lives entirely on the web side — service worker + IndexedDB, covered separately.)
  2. You inherit WebView quirksenv(safe-area-inset-*) often resolves to 0 even with viewport-fit=cover set. Fix: stack a fixed minimum padding on top of the CSS value instead of trusting it alone.

"Finished loading" ≠ "actually works"

onLoadEnd means "navigation finished," not "a human would call this working" — for a React SPA, the shell can be blank while JS boots. So there's a second signal: a 15-second timer, and if still loading when it fires, that's a failure regardless of what the WebView reported.

The real gotcha: onLoadEnd fires after onError too — it's "did-finish," not success-only. Wire it naively and a failed load flashes an error screen then un-fails itself a moment later. Fix is one ref, set the instant an error fires, checked before allowing success to override it.

Retry doesn't call .reload() — it bumps a key prop and lets React fully remount the WebView, after checking real connectivity first. A stale WebView instance can wedge itself into states a soft reload won't clear.

Bridging the back button

Android's hardware back button should navigate the website's own history. Track canGoBack from WebView nav state; on back-press, call goBack() if there's history — and explicitly do nothing (let the OS exit the app) if there isn't. Swallowing back-press unconditionally traps users on a screen with a dead back button, which is worse than not intercepting it.

Making a website stop feeling clickable

Injected JS disables text selection and long-press callouts globally (except real inputs), suppresses the copy/share/lookup context menu, kills link previews, and turns off bounce/overscroll. Same instinct on the web side via -webkit-tap-highlight-color: transparent and overscroll-behavior-y: none — belt and suspenders across the native/web boundary.

Camera permission for a plain HTML input

The upload flow is a bare <input type="file"> — the web app has no idea it's in a WebView. But camera capture from a web file picker needs an OS permission that behaves inconsistently if only requested lazily mid-flow. So the shell proactively requests camera/photo permissions once the page loads, ahead of the user ever tapping upload.

The one screen the theming audit never reached

Small confession: the error screen's retry button is a hardcoded #2E7D5B green. Brand teal everywhere else is #0d9488. Not the same color — and this is the one screen a 21-page design consolidation pass never touched, because it's native code that only renders when something's already gone wrong.


👉 Try MyVitals now — same app, same day, web or home screen icon.

Track your own lab reports

Upload a report from any lab, in any format. MyVitals reads every value and builds one continuous history you can actually follow.

General health information, not medical advice. Always consult a qualified healthcare professional.

Related reading