The Android repos share one idiom for reading keys at build time.
fun secret(name: String, fallback: String = ""): String =
(localProps[name] as String?) ?: System.getenv(name) ?: fallbackI ran a fleet-wide check. Three live apps had shipped with no payment public key at all.
When a value is missing, does your secret reader stop, or hand back an empty string?
An empty string passes every gate
With fallback = "", a missing value passes silently as an empty string.
Compile passes. Tests pass. Lint passes. The app looks healthy. Only payment is dead.
And the paywall's copy flattened every failure into one message:
Couldn't load plans. Please check your connection.
A configuration error read as a network problem. Payment dying quietly also showed up in paid but not pro and the payment was never the problem. That wording guarantees a long misdiagnosis. Users and I read the same sentence and reached the same conclusion: the internet must be slow.
The empty key was a symptom
I dug into why the key was missing. In all three apps, the Android app had never been created in the payment platform's project. Only the iOS app existed.
There was no key to paste in the first place.
It's an easy step to drop when porting an iOS app to Android. You port the code, you create the store listing, and you forget to register the app on the payment side. All three shipped that way.
What would you do?
You need to confirm the key made it into the release. Where do you look?
- The local config file — what I did first. That tells you whether your machine has a value, which is independent of whether it shipped.
- The CI environment variable list — being configured and being injected into that build are different things.
- The shipped artifact — check whether the key pattern is actually inside the release bundle's dex.
Only the third is a verdict. The others are circumstantial.
One more trap: the payment SDK carries a dummy key as a string inside its error guidance. When you grep the artifact for key patterns, don't count that as a real key. I miscounted it once.
Three checks
- Does your secret reader fall back to an empty string? Then a missing config disguises itself as a healthy build. Required keys should fail the build with no fallback.
- Does your payment failure copy distinguish causes? Flatten everything into "check your connection" and you'll misdiagnose config errors as network problems.
- Are you deciding key presence from local config? Decide from the artifact — and don't count the SDK's own dummy key.
The honest part
Three apps sat live unable to take payment. How long, and whether anyone tried to pay during that window, cannot be determined — the attempt fails before SDK initialization, so nothing is recorded.
Which means the loss is uncountable. "Zero revenue," "zero purchase attempts," and "purchasing impossible" occupied the same column in my ledger, and I had been reading it as the first one.