DEV Community

Cover image for I Shipped an Play Store App That Updates Itself, Because There's Nothing in It to Update
Suyash Vashishtha
Suyash Vashishtha

Posted on Originally published at blogs.myvitals.co.in

I Shipped an Play 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';
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)