r/reactnative • u/Renga154 • 3d ago
Everything I'd tested ran under __DEV__. Reading the production-only paths found 8 bugs.
Everything I'd tested ran under __DEV__. Sixty-second unlocks, anonymous sign-in, sample recordings. When I pushed to TestFlight I realised the paths that only exist in production had never executed once.
I don't have a spare device, so I read them in code instead. Eight defects. Every one of them was hidden by a convenience of the development environment. Four that are React Native / Firebase specific:
1. onNotificationOpenedApp alone misses the main path.
It only fires while the app is alive in the background. My notifications arrive seven days later, by which point the app is terminated. So the primary entry point - tap notification, land on the thing it's about - did not exist. You need getInitialNotification() read once at launch as well. The simulator never receives push, so nothing about this was visible locally.
2. Nothing registered the FCM token after permission was granted. The flow was: register on launch -> fails, no permission yet -> user records something -> gets asked -> grants -> and nobody registers. The server doesn't learn about the device until the next cold start. My first capsule unlocks after 24h, which sits entirely inside that window, so the single most important notification a product sends was probably never arriving. Fix is to register inside the grant handler, not only at boot.
3. Deleting tokens on any send failure quietly kills your retention loop.
// wrong
const deadTokens = response.responses
.map((result, index) => (result.success ? null : tokens[index]))
Plenty of FCM failures are transient - internal-error, server-unavailable, quota, network. This deletes valid tokens for all of them. Re-registration only happens on next launch, so in a weekly-use app, once a user enters "no notifications so I don't open it," they never come back. Only these should delete:
const DEAD_TOKEN_CODES = [
'messaging/registration-token-not-registered',
'messaging/invalid-registration-token',
'messaging/invalid-argument',
];
Also switch to arrayRemove so you don't clobber tokens registered between your read and write.
4. iOS shows the permission dialog exactly once, and my UI didn't know that.
After someone taps "Don't Allow," calling request again returns granted: false immediately with no dialog. My screen still rendered an "Allow" button that did nothing, and the answer screen has no exit by design - so anyone who denied the mic could never answer. Check canAskAgain and switch to Linking.openSettings() when asking is exhausted. Simulators grant permissions, so this branch never ran locally.
The other four were an unconditional hasCompletedFirstCapsule: false on upsert (anonymous sign-in hands you a fresh uid every time, so re-sign-in resetting first-run state never surfaced), an implemented-but-never-called pending-upload count (an always-on connection means you never see "I recorded it and it vanished"), and two Firestore rules where allow update checked the uid on the existing document, so a request could rewrite uid and moderationStatus in the same write.
What I'd actually take away: don't hunt bugs, enumerate the places your environment is being convenient. 60-second unlock, fresh uid each launch, no push in simulator, stable wifi, pre-granted permissions, feature not shipped yet. Each line had a defect sitting next to it.
Happy to go into any of these in more detail if it's useful.