DEV Community

Cover image for 11 things that cost me days shipping my first app to both stores
Álvaro Silva
Álvaro Silva

Posted on

11 things that cost me days shipping my first app to both stores

I spent 5 months building **Glowwy**, an app that analyses a photo of your skin and turns it into a score you can track over time. Building it was the easy part.

The last mile — store consoles, subscriptions, compliance, review — took far longer than I budgeted, and almost none of it was written down anywhere I could find. Here are the eleven things that actually cost me days.


1. Google's 14-day closed test decides your launch date, not your code.

New personal Play accounts have to run a closed test with real testers for 14 continuous days before you can ship to production. You can't shorten it and you can't skip it. I found out well after I thought I was nearly done. If you're on Android and haven't started this, start it today — everything else can happen in parallel.

2. The Play service-account page every tutorial points at no longer exists.

Every guide says "Play Console → Setup → API access". That page is gone for newer accounts, and I burned an afternoon convinced I was missing a permission.

The current flow: create a Google Cloud project, enable the Play Developer APIs, create a service account, then in Play Console go to Users and permissions and invite the service account's email address as if it were a colleague. The two consoles are linked by that email — not by a project. Once I understood that, it stopped being mysterious. Permissions then take up to 36 hours to propagate, so do it early.

3. Apple's Paid Applications Agreement silently blocks every subscription.

Until it reads Active in App Store Connect, your subscription products sit in "Missing Metadata", your purchase SDK sees an empty catalogue, and your paywall throws "no offerings available". Nothing anywhere tells you these are related. It's tax forms and bank details, it takes up to 24 hours to go active, and it's the first thing I'd do next time.

4. My purchase code worked perfectly on iOS and would have failed on every Android purchase.

The two stores name products differently:

iOS      pro_monthly     flat identifier
Android  pro:monthly     subscriptionId : basePlanId
Enter fullscreen mode Exit fullscreen mode

I was matching purchases on the raw store product ID. On iOS it matched. On Android pro:monthly would never have equalled pro_monthly, so no purchase would ever have completed. I caught it before shipping, but only by accident.

If you use a layer like RevenueCat, match on its package identifier ($rc_monthly), never the store's product ID. That's the entire reason the abstraction exists.

5. A purchase can succeed while your database still says "free".

My first real test purchase went through. Play charged the test card, the app unlocked, the confirmation screen said "Elite Unlocked". And the backend still had the user on the free tier, so every server-gated feature returned 403 while the UI cheerfully showed them as unlocked.

One unset environment variable. The webhook endpoint had been returning 500 the whole time and nobody was watching. Test the webhook, not just the purchase — and test the expiry too. Everyone tests the upgrade; almost nobody tests the downgrade.

6. Apple requires in-app account deletion. "Email us" is rejected.

If your app lets users create an account, it must let them delete it from inside the app — auth user, database rows, everything. Google doesn't enforce this nearly as hard, so if you build Android-first it won't come up until Apple rejects you.

Two details worth getting right: delete your database rows before the auth user, because deleting the auth user invalidates the session immediately. And warn people that deleting their account doesn't cancel their subscription — the store keeps billing them.

7. EU trader status will remove your app from 27 storefronts if you decline it.

Under the Digital Services Act, anyone selling commercially in the EU — which includes any paid app or in-app purchase — has to declare trader status and publish contact details on the listing. Apple pulled thousands of live apps when the deadline passed.

As a sole trader that means your personal name and address are published publicly. If that bothers you, sort out a business address before you submit, not after you've been verified.

8. My first build died in 54 seconds because of a .gitignore line.

package-lock.json was in .gitignore. Cloud builds run npm ci, which refuses to proceed if the lockfile is missing or has drifted from package.json. Locally everything was fine because npm install is forgiving.

Related, from the same cleanup: node_modules was in .gitignore but had been force-added earlier, so 63,235 files were staged for commit. Ignoring a path doesn't untrack what's already in the index.

9. My app hung forever on a loading spinner. It wasn't my code.

Infinite spinner, no error, nothing in the console. I went through the auth flow line by line.

The emulator had lost DNS. I'd switched the laptop from Wi-Fi to a phone hotspot, and the Android emulator captures its network config at boot and never re-reads it. One line in adb logcat said Unable to resolve host and would have saved me an hour.

Read the device log before you read your own code. I now do this first, every time.

10. My paywall advertised twelve features. Eight of them didn't exist.

This is the one I'm least proud of. I'd written the feature list early, as aspiration, and never revisited it. By launch the paywall was promising a monthly report, a community, export cards and a 90-day history — none of which were built.

Worse: the headline feature had a teaser card for free users and if (isSubscribed) return null underneath. Subscribing made it disappear. Someone paying €4.99 would have got strictly less than a free user.

Both stores prohibit charging for functionality you don't deliver. Audit your own paywall against what actually ships, and do it before a tester finds it rather than after.

For the record, I fixed this before launch: the paywall now lists only what ships, and the features that were worth keeping got built.

11. Apple accepts exact screenshot dimensions only. Google doesn't care.

My store screenshots sailed through Play and were rejected instantly by Apple, because Play accepts a range and Apple accepts a fixed list. Same for the app icon: any alpha channel and the upload is rejected — after the 25-minute build, which is a miserable way to find out.

Capture at native device resolution. Upscaling to hit the required size looks soft on a Retina store page, and your screenshots are the only thing most people will ever see of your app.


None of this is hard. It's just undocumented, spread across four consoles, and mostly invisible until it blocks you.

I wrote the whole thing up properly — every console path, the compliance forms, the checklists, and a symptom-to-cause table for the bugs above. It's 29 pages, and it's €19 if it saves you a week: https://alvarosilva7.gumroad.com/l/shipping-a-mobile-app

Happy to answer anything in the comments.

Top comments (0)