A hybrid app we built needed users to snap a photo of a receipt, straightforward on paper, more annoying in practice once we hit the differences between how iOS and Android handle the camera plugin's returned image data.
The basic requirement
The feature itself sounds trivial: tap a button, open the camera, take a photo, attach it to an expense report being submitted through the app. We assumed, wrongly, that the camera plugin would behave close enough to identically across platforms that we could write one capture function and call it done.
Where iOS and Android diverged
- On iOS, base64 image data came back reliably but slowed the app down noticeably for larger photos, a full-resolution receipt photo encoded as base64 could take a visible second or two to process and hand back to our JavaScript, which felt sluggish compared to a native camera app's near-instant capture.
- On Android, using FILE_URI instead of base64 output avoided the memory spike, since we're just getting a path to a file already written to disk rather than a giant string held in memory, but it needed extra handling to make sure the file path was actually readable afterward, permissions and storage location varied more across Android manufacturer skins than we expected.
- We ended up branching the capture options per platform rather than finding one setting that worked cleanly everywhere, checking device.platform and configuring the Camera.getPicture call differently for each.
The part that actually took the longest
Not the branching logic itself, that was a few hours of work once we understood the problem. The longest part was reproducing the Android storage permission issue reliably enough to actually fix it, it only showed up on certain devices with certain Android versions, and a bug that only reproduces on one specific test device is a genuinely miserable thing to debug compared to something that fails consistently everywhere.
Compressing the image before upload
Once capture was working reliably on both platforms, we still had an upload problem, a full-resolution photo from a modern phone camera is several megabytes, and uploading that over a spotty mobile connection at a job site, which is where this client's field staff would actually be using the app, took long enough to feel broken. We added a client-side resize step before upload, scaling anything wider than 1200 pixels down before it ever leaves the device, which cut typical upload sizes dramatically without any visible quality loss for what's ultimately just a receipt someone needs to read back later.
Handling the case where the user cancels
An easy thing to miss in testing: what happens when someone opens the camera and then backs out without taking a photo. The plugin fires an error callback in that case, and our first pass didn't handle it, resulting in a stuck loading spinner if a user changed their mind partway through. A little targeted testing, deliberately backing out of the camera UI instead of only testing the happy path, caught it before it shipped, and it's now part of our standard checklist for any camera or file-picker integration going forward.
The tradeoff we made peace with
Not the cleanest code we've shipped, some very obvious if (device.platform === 'iOS') branches scattered through what's otherwise meant to be shared code, but it's the kind of pragmatic compromise hybrid development seems to ask for. We'd rather ship working, slightly inelegant platform-specific branches than spend days chasing an abstraction that hides the platform difference but breaks in some edge case neither platform's documentation mentions.
Testing across a wider device matrix
Once the core capture flow worked on our own test devices, a fairly recent iPhone and a mid-range Android phone, we borrowed a small pile of older devices from friends and a local phone repair shop for a more realistic spread, an older Android phone running an outdated OS version, a bargain-tier Android tablet, and an older iPhone still in active use by some of the client's own field staff. The bargain Android tablet was the one that broke things, its camera app returned an image orientation flag our code wasn't reading, so every photo taken in portrait came back rotated ninety degrees when displayed in the app. That's a strong argument for testing on genuinely low-end hardware specifically rather than only the newer devices sitting on our own desks, since the bugs that show up there are exactly the ones a demo on a nice phone will never surface.
The photo orientation bug, in more detail
The fix ended up being straightforward once we understood the actual cause, reading the EXIF orientation tag from the image data ourselves and rotating the canvas before rendering a preview, rather than trusting that every device's camera app writes an already-rotated image. It's a well-known enough problem in mobile photo handling generally that we probably should have anticipated it from the start rather than discovering it via a borrowed tablet, and it's now the first thing we check on any feature that captures or displays a user photo.
What we'd build differently with more time
If we were starting this feature over with what we know now, we'd reach for a single, well-tested third-party camera wrapper plugin rather than writing our own platform-branching logic against the base Camera API, several have matured since we started this project and appear to already handle the orientation and file-URI-versus-base64 tradeoffs we spent real time working through ourselves. We didn't have that option when we started, the available plugins were rougher, but revisiting this decision periodically as the plugin ecosystem matures rather than assuming our original choice is still the best one available is now part of how we approach any hybrid app dependency.
Storage cleanup, a problem we found late
One more thing we almost shipped without noticing: every captured photo, plus its resized copy, was being written to the device's local storage and never cleaned up after a successful upload. On a phone used daily for weeks, that adds up to a meaningful amount of wasted storage for photos nobody will ever need again once they're safely on the server. Adding an explicit delete step after a confirmed successful upload fixed it, and we now treat "does this feature clean up after itself on the device" as a standard question for any hybrid app feature that writes local files, not just this one.
Debugging tools that actually helped
Remote debugging turned out to matter more here than on almost any other part of this app, since camera behavior can't be meaningfully tested in a desktop browser at all. Being able to plug an iOS device into a Mac and inspect the WebView through Safari's developer tools, and the equivalent remote debugging setup for Android, meant we could actually see console output and inspect the DOM on the physical device where the bug was happening, rather than guessing blind from reported symptoms alone. Before we had this set up properly, debugging a device-specific issue meant a slow cycle of adding an alert call, rebuilding, reinstalling, and reading a popup, and cutting that down to actual live debugging tools probably saved us a full day across this feature alone.
A note on user permission prompts
iOS and Android also differ in when and how they ask the user for camera permission, and getting that prompt to fire at a sensible moment, when the user actually taps the capture button, rather than immediately on app launch before they have any context for why the app wants camera access, took some digging through both platforms' permission models. A permission request with no context attached gets denied far more often than one presented right when the user is about to use the feature it's for, and re-requesting a previously denied permission requires sending the user to a settings screen rather than simply asking again, which shaped how we designed the whole capture flow around asking at exactly the right moment rather than defaulting to whatever the plugin does out of the box.
One more platform quirk worth naming
A last oddity we ran into: on one Android device, denying the camera permission once and then granting it later in the phone's settings didn't reliably re-trigger our app's own permission-check logic without a full app restart, which meant a user who changed their mind mid-session saw the feature silently fail to work rather than succeed as expected. We ended up adding an explicit "try again" path that re-checks permission state rather than assuming a single check at app launch is enough, a small addition that only surfaced because we tested the change-your-mind path deliberately rather than only the straightforward allow-once flow.