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'; 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 - 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.) - You inherit WebView quirks - env(safe-area-inset-*) often resolves to0 even withviewport-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 - 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)
Comments
No comments yet. Start the discussion.