MMashDiv

Differentiation

Apple 4.2.6 as a shipping requirement rather than a theme — what must differ, the one file and two commands that make the app yours, the release preflight that fails while the project is still the template, and the parts no build step can check.

2 min readUpdated 26 September 2026mobile, 4.2.6, branding, icon, preflight, app-config

Apple 4.2.6 does not ban template-built apps. It requires the template provider to give you customisation tools, and requires you to use them. An app that differs from every other build of the same template only by its logo is the case the rule names, and 4.3(b) allows removal of one that is already live.

So differentiation is a shipping requirement here, not a preference.

The tools you are given

Your app's name, its two store identifiers and its brand colours are keys in one file, assets/config/app_config.json. The icon is one image. You do not edit AndroidManifest.xml, Info.plist, build.gradle.kts, the Xcode project or app_themes.dart: the build reads the config and carries it onto both platforms.

What Set it in Then
The name, inside the app and on both home screens appName Nothing. The next build applies it on Android and iOS.
The Android identity androidApplicationId Nothing. Gradle reads it on every build.
The iOS identity iosBundleId dart run tool/apply_app_config.dart, which writes it into the Xcode project
The colours primaryColor, buyColor, sellColor, accentColor Nothing. The next build carries them, and the app reads them when it starts.
The icon a square PNG of at least 1024×1024 at assets/icons/app_icon.png dart run tool/apply_app_config.dart --icons

Choose both identifiers before your first release: neither store lets an app change its identifier afterwards. Every key, what each platform does with it, and how to move an older hand-edited copy onto the config are on Make it your app.

The release preflight

From the project root, before every release build:

dart run tool/release_preflight.dart --explain

It exits non-zero, with the fix for each item, while any of these is still the template's. Each finding starts with its tag in square brackets:

Checked Finding Why
appName is set, and is not the template's (BiCrypto, Mobile, YourAppName or Your App Name, in any case) app-name One name now reaches the app and both home screens, so this is the one place to ask whether it is still the template's.
Nothing overrides the name: a typed-in android:label in AndroidManifest.xml, a build.gradle.kts older than the manifest, a Runner target without the Apply app_config.json build phase android-label-hardcoded, android-label-placeholder-missing, ios-name-phase-missing Each passes every other check while putting a second name on a home screen — or, for the Gradle case, failing every Android build. These are what a half-finished upgrade from an older release leaves behind.
The Android applicationId the build will really use, and the iOS PRODUCT_BUNDLE_IDENTIFIER in the Xcode project bundle-id Fixed after your first release on either store. Anything under com.bicrypto or com.bicryptto is refused.
A typed-in applicationId in build.gradle.kts that beats androidApplicationId, and an Xcode project that disagrees with iosBundleId android-id-hardcoded, ios-bundle-id-mismatch The config says one identity and the build ships another. The iOS case also stops every iOS build at the Apply app_config.json phase.
assets/icons/app_icon.png app-icon, app-icon-size The file the package ships is 281 bytes of text, not an image. A source under 1024×1024 is refused too, because flutter_launcher_icons upscales silently and the result is your store listing.
The generated launcher icons (the Android mipmap-xxxhdpi icon and the iOS 1024×1024 App Store icon) launcher-icon What ships is the generated files, not your source image. A new image without --icons still ships the template's icon, and passes the check above.
At least one of the four colour keys set to a colour of your own palette This detects that nobody chose a palette. It cannot grade one and does not try.
baseUrl backend-url A build submitted against the placeholder address or a vendor demo has the reviewer looking at somebody else's data.
app_config.json exists and every value in it is valid config-missing, config-unreadable, config-invalid A value the build or the app would refuse is reported here in the same words dart run tool/apply_app_config.dart --check uses.

Five more are not about differentiation, and are here because this is the command you run right before a release build:

Checked Finding Why
android/key.properties exists and names all four of storeFile, storePassword, keyAlias, keyPassword release-signing Without it the release build is signed with the debug key, succeeds, and produces an artifact Google Play refuses. Gradle's warning about it is hidden by flutter build, so nothing on screen says so.
compileSdk and targetSdk are written as a number, at least 36, in android/app/build.gradle.kts (not inherited from the installed Flutter) android-target-api Google Play requires target API 36 from 31 August 2026. An inherited value depends on whichever Flutter is installed.
When ios/Runner/GoogleService-Info.plist is present: a Runner.entitlements with aps-environment, wired into the project ios-push Without it the iOS permission prompt still appears, the customer agrees, and no notification is ever delivered.
The Apple signing team in the Xcode project: not the team older releases shipped with (52K7QMD7KR), present for the Release configuration, and equal to iosTeamId when that is set ios-team, ios-team-mismatch An archive signed with somebody else's team fails, and one with no team signs only if Xcode happens to pick the right identity.
baseUrl is https:// and wsBaseUrl is wss:// backend-not-encrypted A release build with http:// or ws:// opens on the Configuration Error screen instead of starting.

If you publish only on Google Play, add --android-only. It skips the iOS checks (ios-team, ios-team-mismatch, ios-push and the other iOS findings) and runs every Android one, and the run begins with release preflight: --android-only, so the iOS checks were skipped.

Without --explain, an untouched template fails like this (with it, each line is followed by its fix):

RELEASE PREFLIGHT FAILED — 10 item(s) still ours, not yours:

  [app-name] app_config.json still calls the app "BiCrypto"
  [bundle-id] Android applicationId is still "com.bicrypto.mobile"
  [bundle-id] iOS PRODUCT_BUNDLE_IDENTIFIER is still "com.bicryptto.mobile"
  [ios-team] the Xcode project names no Apple Development Team for the Runner Release configuration
  [release-signing] android/key.properties is missing, so a release build is signed with the DEBUG key
  [launcher-icon] the Android launcher icon is still the one this template ships
  [launcher-icon] the iOS App Store icon is still the one this template ships
  [app-icon] assets/icons/app_icon.png is the shipped placeholder, not an image
  [palette] the brand colours are still the template's: app_config.json sets no primaryColor, buyColor, sellColor or accentColor of its own
  [backend-url] baseUrl is still "https://your-backend-url.com"

A build that passes prints release preflight: this build has been differentiated from the template.

The installers run it at the end and report; that run is advisory, because an installer run configures a development build and you may not have chosen an icon yet. The run before a release build is the one that matters.

What no build step can check

These are the parts that decide a borderline review:

  • Your store listing. Screenshots taken from the template, in the template's colours, are the most recognisable signal that a reviewer is looking at the same app for the fifth time this month. Take your own, on your own deployment, with your own data.
  • Your listing copy. Your positioning, your markets, your differentiators — not a description of the platform's features.
  • Your feature set. Which optional modules the app shows is decided by your own server (GET /api/user/modules), see What is in the app. Turning off what you do not offer is accurate, and it makes your app materially different from the next operator's.
  • Your own screens. You have the full Flutter source. A screen that exists only in your build is the strongest 4.2.6 answer there is.

The preflight also cannot tell a considered palette from a hue rotation, or a real icon from a recoloured one. It checks that each decision was made, not that it was made well.

The failure case, stated plainly

A default-skinned build with a swapped logo, submitted alongside a dozen others that look the same. If that describes your plan, the differentiation work is cheaper than the rejection.