Koko, KMP Starter Kit — Part 3: Publishing: Your App in the Stores

Part 3 of 4: the third phase of the KMP contest starter kit. What to check when publishing, what the stores ask for, and how to publish both apps from GitHub without having a Mac.

New here? Part 1 introduces the kit and gets your app running on your machine. Part 2 connects it to Firebase, sign in and a backend.

You’ve finished the development part of your app, tested it, and it works. Screens done, data saved, maybe some services connected from part 2. Now comes the publishing part, and it can look complicated at first. But publishing is mostly a checklist, and the kit ships the checklist as a script:

./MobileApp/scripts/check_env.sh --phase publishing

Or ask your AI coding assistant “is my app ready to publish?” and it runs this instead of guessing. It checks the things people forget: are the privacy policy and terms URLs real pages instead of placeholders? Is the contact email still support@example.com? Is your AI feature still calling providers directly with a key that would ship inside the binary? Each warning points at the fix. The rest of this post goes through the phase in order. A few steps involve waiting, so it’s better to read it once first and start those early.

Store accounts

You need a developer account with each store. Google Play: $25, once. Apple: $99 per year.

Start both today. Apple or Google can take days to verify a new account. Google Play has a testing rule: if you register as an individual (not a company), you can’t publish to production until your app has run a closed test with at least 12 testers for 14 days, and then you apply for production access. Company accounts skip this, but need a registered business with a D-U-N-S number. For most first apps: register as an individual, collect 12 friends’ emails now, and use those two weeks as your beta.

App id and configuration

Two things become permanent in this phase.

The app id. Google Play ties your package name to the app on the first upload, and Apple ties the bundle id to the store record. Neither can be changed afterwards. If you’re still using a name you picked quickly in part 1, change it now with the same rename script:

./scripts/refactor_package.sh --app-id com.you.pianospot --app-name PianoSpot

The configuration. Open AppConfiguration.kt and fill in:

  • URL_PRIVACY_POLICY and URL_TERMS_CONDITIONS: real, working pages. Both stores reject placeholder links.
  • CONTACT_EMAIL: your support email.
  • PREMIUM_FEATURES_ENABLED: false if nothing in your app is paid. The check script then stops asking for subscription keys.
  • APPSTORE_APP_ID: filled later, when App Store Connect gives you the number.

If you don’t have a website for the privacy policy and terms, the Web/public folder has a landing page with both pages included. Deploy it to Firebase Hosting for free and copy the two URLs into the config.

Before you build the release

  • Icons. Give one square logo, at least 1024 pixels. Ask the assistant to “generate the app icons from logo.png” and the kit creates all Android sizes, the adaptive icon, the iOS icon set and the splash screen logo.
  • Notification icon. Android shows it as a white shape in the status bar, so it must be a simple single color shape. The file is ic_notification.xml.
  • Version. Below script updates both platforms together:
./scripts/update_version.sh
  • AI keys. If you used a direct OPENAI_API_KEY or REPLICATE_API_KEY for testing in part 2, remove it and set CLOUD_FUNCTIONS_URL to your deployed backend. A direct key gets compiled into the app and can be exposed.
  • Feature flag defaults. FeatureFlagManager holds the values the app ships with default values, like whether ads and analytics are on.
  • Quality checks. The same formatting, test and build checks from part 1 should pass.

Signing

Every released app must be signed. Signing proves that updates come from you. For Android, the kit can create the keystore with one command:

./scripts/generate_android_keystore.sh "Your Name" "Your Org (optional)"

This creates keystore.jks and a keystore.properties file with generated passwords. Both are ignored by git, and the build picks them up automatically. To check, build a release:

./gradlew :androidApp:bundleRelease

That produces the signed AAB file that Google Play accepts.

Back up the keystore and its passwords in safe place. Keep them in a password manager or an encrypted drive, not in the repository.

iOS signing can be handled by the GitHub workflow, explained below. You don’t need to create or export a certificate yourself.

What the stores ask for

Google Play:

  • App icon, 512×512
  • Feature graphic, 1024×500. This is the banner at the top of your Play listing, and it’s required.
  • 2 to 8 phone screenshots (tablet screenshots too if you support tablets)
  • Title (30 characters), short description (80), full description (4000)
  • Data safety form: what data you collect and why. With the kit’s defaults, analytics and crash reports mean device identifiers and usage data, and sign in means account information.
  • Content rating questionnaire
  • Target audience, and an ads declaration if you use AdMob
  • Privacy policy URL

Apple:

  • App icon, 1024×1024
  • Screenshots for 6.9 inch and 6.5 inch iPhone (and 13 inch iPad if you support iPad)
  • Name, subtitle (30 characters), keywords (100 characters, comma separated), description, support URL
  • Category, age rating questionnaire, price
  • App Privacy section, Apple’s version of the data safety form
  • App Review information: your contact details, and a demo account if your app needs a login. The kit signs users in anonymously by default, so usually none is needed.

Screenshots. Stores want exact pixel sizes per device. The kit renders your real screens at those sizes from code:

./scripts/generate_store_screenshots.sh

Output goes to distribution/store_screenshots/, sorted by language and device. One run for the demo app produces four screens at the 6.5 inch iPhone size. Both stores accept plain screenshots. If you want designed ones with headlines and phone frames, build them on top of these images in a design tool.When App Store Connect creates your app, it shows a numeric Apple ID. Copy it into APPSTORE_APP_ID in AppConfiguration.kt. The kit uses it for the rate prompt and the manage subscription link.

Publishing from GitHub

Push a tag, GitHub builds and signs the app and sends it to the stores

The kit includes two GitHub Actions workflows. Push a git tag ending in -android and GitHub builds the Android app, signs it and uploads it to Google Play. Push a tag ending in -ios and it does the same for iOS.

iOS apps can only be built on macOS. GitHub provides Mac machines for the build, and the workflow runs there. So you can publish to the App Store from Windows or Linux. To test on a real iPhone, install the build through TestFlight.

The workflow also handles iOS signing. Normally you export a certificate from a Mac, create provisioning profiles and copy team IDs. The kit uses fastlane match instead: it creates the certificate on GitHub’s Mac from your App Store Connect API key, encrypts it, and stores it in a private GitHub repository you create for this. Later releases and your other apps reuse it.

Setup, once per Apple account:

  1. In App Store Connect, create an API key with the App Manager role. Download the .p8 file; Apple shows it only once.
  2. Create an empty private GitHub repository, for example ios-certificates, and a personal access token with read and write access to it.
  3. Add six secrets to your app repository: key id, issuer id, the .p8 as base64, a password you invent, the certificates repository URL, and the token as base64. The setup-signing guide lists the exact names and the commands that produce each value.
  4. For the first run only, add a repository variable MATCH_READONLY=false so the workflow can create the certificate. Delete it afterwards.

You don’t need to remember any of this. The setup-signing skill has every secret name and the command that produces each value, and your AI assistant walks you through it step by step, stopping at each part only you can do.

Why the separate repository and the read only default: Apple gives each account only two distribution certificates. One certificate is kept for the whole account and shared by all your apps, and normal releases can’t create new ones.

The Android workflow needs three secrets: the keystore and its properties file as base64, and a Google Play service account file. The first upload to a new Play app must be done manually in the console; after that the workflow uploads.

Two more things:

  • Every key in local.properties.example needs a repository secret with the same name. If one is missing, the build still succeeds but that feature ships with a placeholder value, so sign in or the paywall doesn’t work. The workflow prints a warning listing missing secrets.
  • Both workflows can also be started manually from the Actions tab. The iOS one lets you choose TestFlight or App Store, and whether to submit for review.

If you’d rather release from your own laptop, the same Fastlane commands run locally, without the secrets setup.

# Android
fastlane android first_time_build # builds the AAB for the manual first upload
fastlane android playstore_release # upload to internal testing track
fastlane android playstore_release track:production # publish to production

# iOS (needs a Mac with Xcode)
fastlane ios appstore_release # archive + upload to App Store Connect / TestFlight
fastlane ios appstore_release submit_for_review:true # also submit for review

After the upload

Android. The build appears in the internal testing track. Move it to closed testing with your 12 testers for 14 days (individual accounts), apply for production access, promote to production, and send for review. Review usually takes a few hours to a couple of days.

iOS. The build appears in TestFlight. Attach it to your 1.0.0 version and submit.

Google sign in in Play builds

Google sign in works in debug builds but fails in the app installed from Play Store. The reason: Google Play signs your app again with its own key, and Firebase doesn’t know that key yet.

Fix: in Play Console, go to Test and release → Setup → App signing, copy the app signing key certificate SHA-1, and add it as a fingerprint to your Android app in the Firebase console (Project settings → your app → add fingerprint). Do this before your testers install.

If you get rejected

It’s usually something small: a missing privacy answer, a screenshot that doesn’t match the app, a permission you didn’t explain, a login the reviewer couldn’t get past. Fix it and submit again. Most apps get rejected at least once.

What this phase costs

$25 once for android and $99 per year for iOS for the accounts. About a weekend of forms and setup, plus Google’s two weeks of testing for individual accounts. The second release is much faster, since everything is already in place.

KMP Starter kit is free and open source: github.com/KotlinFoundation/kmp-contest-starter-kit, with full documentation at https://kotlinfoundation.org/kmp-contest-starter-kit-documentation/.


Koko, KMP Starter Kit — Part 3: Publishing: Your App in the Stores was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.