Release & Play Store
Sign the app with your upload key, build the Android App Bundle, publish it on Google Play (store listing with the assets in store/playstore, App content, Data safety, testing tracks) and register the Play App Signing SHA in Firebase.
The app has no in-app purchases. Packages and featured ads are sold on the website only, so Google Play Billing rules do not apply and you answer No to digital purchases in the Play forms.
Before you build
- Own package name set and FlutterFire run for it (Change Package Name, Firebase Setup)
- App name, launcher icon, splash and the hard-coded
classiadshub.web.applinks replaced (App Name, Logo & Splash) - API base URL points at your production API (API Configuration)
- Your own Maps key, Maps SDK for Android enabled, key restricted (Google Maps & Location)
- Google provider enabled and the new
google-services.jsonin place, if you keep Continue with Google (Login, Register & OTP) - Website payment gateways on live keys and Sandbox / Bank Transfer switched off as needed (Payment Gateways)
- Privacy Policy, Terms and About pages written in the Admin Panel (CMS Pages). The privacy page must name your app, list the data collected (see Data safety) and explain account deletion
- Test data from QA runs removed from the live database (
tools/qa/cleanup-qa-data.js, run it with--dryfirst) - The version text in Settings → About (
settings_screen.dart) matchespubspec.yaml
1. Create the upload keystore (once)
keytool -genkey -v -keystore upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 -alias uploadKeep the file outside the repository or in a git-ignored folder.
2. key.properties
storePassword=YOUR_STORE_PASSWORD
keyPassword=YOUR_KEY_PASSWORD
keyAlias=upload
storeFile=/absolute/path/to/upload-keystore.jksstoreFile may also be relative; it is resolved from android/app/.
3. Signing in build.gradle.kts
Release signing is already wired. android/app/build.gradle.kts loads key.properties and creates a release signing config from it:
val keystoreProperties = Properties().apply {
val file = rootProject.file("key.properties")
if (file.exists()) file.inputStream().use { load(it) }
}
signingConfigs {
if (keystoreProperties.isNotEmpty()) {
create("release") { /* keyAlias, keyPassword, storeFile, storePassword */ }
}
}
buildTypes {
release {
signingConfig = if (keystoreProperties.isNotEmpty()) signingConfigs.getByName("release")
else signingConfigs.getByName("debug")
}
}4. Build the App Bundle
Set the version first. The number after + is the Android versionCode and must increase with every upload:
version: 1.0.0+1 # next release: 1.0.1+2cd mobile
flutter clean
flutter pub get
flutter build appbundle --release
# optional: --dart-define=API_BASE_URL=https://www.yourcompany.com/api/v1The bundle is mobile/build/app/outputs/bundle/release/app-release.aab. Check that it is signed with your upload key:
keytool -printcert -jarfile build/app/outputs/bundle/release/app-release.aabThe SHA-1 must be your upload key's, not the debug key's.
5. Create the app in Play Console
- play.google.com/console (opens in a new tab) → Create app: your app name, default language, App, Free. Accept the declarations.
- Test and release → Setup → App signing: keep Google-managed app signing (Play App Signing).
6. Store listing
Grow users → Store presence → Main store listing. Ready-made assets are in `store/playstore/`:
| Play Console field | File | Spec |
|---|---|---|
| App icon | icon-512.png | 512 × 512 PNG |
| Feature graphic | feature-graphic.png | 1024 × 500 PNG |
| Phone screenshots | phone-screenshots/01-home.png … 08-my-ads.png | 8 screenshots, 1080 × 1920 (9:16) |
| Promo video | promo-30s.mp4 | Play only accepts a YouTube URL: upload it to YouTube as Public or Unlisted and paste the link |
The screenshots tell the story in order: home, listings, ad detail, category, chat, post ad, ad detail, my ads. They show the ClassiAds-Hub branding. Replace them with captures of your branded app before you publish. store/compose.mjs builds framed screenshots from raw captures in store/raw/ (node store/compose.mjs).
Also fill: short description (80 characters max), full description, category (for example Shopping), contact email and website.
7. App content
Policy → App content. Suggested answers for this app:
| Section | Answer |
|---|---|
| Privacy policy | Your website's privacy page, for example https://www.yourcompany.com/page/privacy |
| Ads | No, unless you add an ad SDK. The app has only an empty AdMob placeholder |
| App access | Restricted: give the reviewer a test login (email + password) and note "Tap Continue with Email on the login screen" |
| Content rating | Category All other app types. User-generated content Yes, users interact Yes, shares location Yes, digital purchases No |
| Target audience | 18 and over (a marketplace with chat between strangers) |
| Financial features | None |
| Account deletion | Yes, in the app (Profile → Delete Account) plus a web URL, for example your website's profile page |
Data safety
All traffic is encrypted in transit (HTTPS / Firebase). Users can delete their account.
| Data type | Collected | Purpose | Required? |
|---|---|---|---|
| Name | Yes | Account management, App functionality | Required |
| Email address | Yes | Account management | Required (or phone) |
| Phone number | Yes | Account management, App functionality | Optional |
| Approximate and precise location | Yes | App functionality (location label, nearby ads) | Optional |
| Photos | Yes | App functionality (ad photos, avatar) | Optional |
| Messages (in-app chat) | Yes | App functionality | Optional |
| Files and docs (ID documents for verification) | Yes | App functionality (seller verification) | Optional |
| Device or other IDs (FCM token) | Yes | App functionality (push) | Required |
| App interactions (Firebase Analytics) | Yes | Analytics | Required |
| Crash logs and diagnostics (Crashlytics) | Yes | App functionality, Analytics | Required |
Data is not shared: Firebase and Google act as your service providers. Review this table against your own privacy policy and any SDKs you add.
8. Upload and test
Internal testing
Test and release → Testing → Internal testing → create a testers list → Create new release → upload
app-release.aab→ release name1.0.0 (1)→ release notes → Save → Review → Start rollout.Register the App Signing SHA
See the next section. Do it before you test Google sign-in and Maps from Play.
Test on a real phone
Install from the opt-in link and check: email login, Google sign-in, the map on an ad, a push on a new chat message, posting an ad with a photo, and Delete Account.
Closed test (new personal accounts)
New personal developer accounts must run a closed test with at least 12 testers for 14 continuous days before production access. Organisation accounts skip this.
Production
Production → Create new release → promote the tested release, choose countries, use a staged rollout (for example 20%) → Send for review. The first review can take from a few days up to about a week.
Add the App Signing SHA to Firebase
Google re-signs every app delivered from Play with the App signing key. After the first upload:
- Play Console → Test and release → App integrity (App signing) → copy the App signing key certificate SHA-1 and SHA-256.
- Firebase Console → Project settings → Your apps → Android app → Add fingerprint → add both.
- Download
google-services.jsonagain and replacemobile/android/app/google-services.json(needed for Google sign-in), then build and upload a new version. - Google Cloud console → your Maps key → Android apps restriction → add the same SHA-1 with your package name.
After launch
- Crashlytics in the Firebase Console for crashes, Play Console → Android vitals for ANRs.
- For each update: bump
version:(the+number must increase), runflutter build appbundle --release, create a new release in the same track. - If you ever change signing keys, update the SHA fingerprints in Firebase and the Maps key restriction.
iOS (App Store)
The project builds for iOS (deployment target 13.0), but the release flow is not scripted. In short:
- Open
mobile/ios/Runner.xcworkspace, set your Team and bundle ID, and add Push Notifications and Background Modes → Remote notifications if you use push. - Add
NSPhotoLibraryUsageDescriptiontoInfo.plist(the image picker needs it; it is missing in the shipped file). - Upload an APNs key to Firebase, and use an iOS-restricted Maps key in
AppDelegate.swift. - If you offer Google sign-in on iOS, App Review may require Sign in with Apple (Login, Register & OTP).
flutter build ipa --release, then upload with Xcode Organizer or Transporter and submit in App Store Connect.
Next: Architecture →
