MMashDiv

Troubleshooting

An app where every screen fails, the Configuration Error screen, prices that never move, coins drawn as letters, customers signed out, pushes that never arrive, builds that stop and stores that refuse the upload — each with its cause, the fix and the check that proves it.

28 min readUpdated 26 September 2026troubleshooting, mobile, flutter, websocket, push, build, signing, app-store, google-play

Sorted by how often each problem reaches a support queue. Every item gives the symptom as the app or the build prints it, the cause, the fix, and a check that proves the fix worked. Most app complaints look the same from the outside ("nothing loads"), but they have different causes and need different fixes.

Two facts explain much of this page:

  • The configuration is packed into the app when you build it. Editing assets/config/app_config.json changes nothing on a phone until you stop the app, build again and install the new build. A hot restart does not reload it.
  • The app reads everything from your server. Prices, screens, logos, sessions and pushes all come from the host in baseUrl and wsBaseUrl. A proxy that serves the website correctly can still break the app.

Start here: three checks

  1. Which build is on the phone? Tap the gear (settings) icon at the top right of Home, then Open-source Licenses. The page shows the app name and the version as 5.3.9 (10): the version: line of pubspec.yaml, as name (build number). Builds before 5.3.9 showed the hand-typed appVersion value from the config instead, which could be wrong.

  2. Is the configuration the one you think it is? From the project root:

    dart run tool/apply_app_config.dart --check
    dart run tool/release_preflight.dart --explain

    The first command changes nothing. It lists every invalid value in app_config.json at once; when there is none, it prints the name, IDs, colours and icon each platform will use. The second command reports anything still left at the template's values, including a baseUrl that is still a placeholder (backend-url), and an http:// or ws:// address (backend-not-encrypted). If you build only for Android, add --android-only to skip the iOS checks.

  3. Does your server answer the way the app needs? From a computer outside the server:

    # The market list the app loads at start-up. Must be 200 and a JSON array.
    curl -s -H "platform: mobile" "https://exchange.example.org/api/exchange/market?eco=true" | head -c 200
    
    # WebSocket upgrades through your proxy. Must print a 101 line.
    # --http1.1: this upgrade is an HTTP/1.1 handshake, and curl may pick HTTP/2.
    curl -i -N -o - -s --http1.1 \
      -H "Connection: Upgrade" \
      -H "Upgrade: websocket" \
      -H "Sec-WebSocket-Version: 13" \
      -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
      https://exchange.example.org/api/exchange/ticker | head -1
    
    # Coin art. Must return the image, not a 404.
    curl -sI https://exchange.example.org/img/crypto/btc.webp | head -1

    Use the exact hosts in your baseUrl and wsBaseUrl.

"No internet connection" on a phone that is online, and every screen fails

Markets are empty, the wallet says there are no wallets, and every screen shows a network or server error. Either the app is talking to the wrong host, or it cannot reach the right one.

Cause 1: the build is running on the example configuration. When assets/config/app_config.json is missing or empty at build time, the app does not stop. It falls back to assets/config/app_config.example.json, whose baseUrl is the placeholder https://your-backend-url.com, and every request goes there. The phone shows no warning. The only sign is this line in the debug log:

⚠️ MAIN: assets/config/app_config.json was not in the bundle, so the app is running on assets/config/app_config.example.json. Every request goes to the placeholder host https://your-backend-url.com. ...

Cause 2: the config file is the copy that ships in the zip. Every release zip contains an app_config.json that is a copy of the example. A build made straight from a freshly unzipped release, or from a tree where the release's assets/ folder was copied over yours, points at the placeholder. The file exists, so there is no log line this time. The preflight reports it as backend-url.

Cause 3: baseUrl names a host that does not serve /api. If the host only serves the website, or is a different host altogether, requests come back as 404s. The screens show errors such as Resource not found or Failed to fetch markets: ....

Cause 4: the host cannot be reached. A DNS name that does not resolve, a firewall or a server that is down. The app reports these as No internet connection or Connection timeout. It uses these words for any failure to connect, even when the phone's own connection is fine. If the market list cannot connect at start-up, a banner across the top of every screen reads Unable to connect to server. Using offline mode.

Fix. Set baseUrl to your site address, the address customers open in a browser (for example https://exchange.example.org), and wsBaseUrl to the same host with wss://. That host serves the website, /api, the coin art and uploads. Then stop the app and build again.

Confirm. dart run tool/apply_app_config.dart --check passes, the preflight no longer lists backend-url, and the first curl in Start here returns a JSON array. In a debug run, the log shows ✅ MAIN: App configuration loaded successfully and no was not in the bundle line.

The banner Server is under maintenance. Using offline mode. appears when the start-up market list fails with an error that mentions 503, service unavailable or maintenance, or a WebSocket that was not upgraded. It clears the next time the market list loads successfully.

The app opens on "Configuration Error"

A dark screen with a red icon, the heading Configuration Error, the line Failed to load app configuration, a box with the exact problem, and this advice:

Fix the setting named above in assets/config/app_config.json. If that file does not exist yet, copy assets/config/app_config.example.json to it and set your own baseUrl and wsBaseUrl. Then stop the app and build again.

Nothing else starts: no sign-in screen, no network. The box shows the message from the list below, starting with Exception:. Fix the key it names, then stop the app and build again.

"... is not valid JSON."

A trailing comma, a missing comma or quote, a comment, or curly "smart" quotes pasted from a document. The file must be strict JSON. On Android the build usually stops on this first, with assets/config/app_config.json is not valid JSON (at line N column M) and a hint. dart run tool/apply_app_config.dart --check reports the line number too. Unlike a missing file, a broken file never falls back to the example.

"... does not set "baseUrl"." (or "wsBaseUrl")

Both keys are required and have no default. The full message suggests "https://exchange.example.org", or, in a debug build only, "http://10.0.2.2:4000" for an Android emulator reaching a backend on your own computer. A trailing slash is removed for you.

"... sets "baseUrl" to "http://...", which is not a https:// address, and this is a release build."

The same stop exists for wsBaseUrl with wss://. A release build talks to its backend only over https:// and wss://, and stops here, naming the key, when either address uses anything else. The reason is in the message: the sign-in token travels with every request, and nothing in the app or the phone refuses a plain connection, so a release build on http:// would send every session in the clear and look healthy doing it. Debug and profile builds accept http:// and ws:// for testing against a local backend. Put your server behind TLS (SSL) and use the https:// and wss:// addresses.

To catch this before you build, run dart run tool/apply_app_config.dart --check --release. Without --release the tool only warns about these two addresses. The preflight reports them as backend-not-encrypted.

"... sets "primaryColor" to "...", which is not a colour."

The same applies to buyColor, sellColor and accentColor. A colour must be #RRGGBB or #AARRGGBB (alpha first). An empty string counts as not set and gives the default colour. Any other value, such as "blue", "0ECE7A" or "#12345", stops the app here, because painting the app in the default colour instead would hide the mistake. See Branding.

"... sets "appName" to ..., which is not text."

The name must be a string in quotes. Remove the line to use the default, BiCrypto.

"... parsed, but a value in it has the wrong type."

A value is valid JSON but the wrong kind. Usually a switch written in quotes ("googleAuthEnabled": "false") or a number written as text ("settingsCacheDuration": "3600"). Remove the quotes around true, false and numbers. The error after Error: names the type that was expected.

"... Please ensure assets/config/app_config.json exists and is valid."

Neither the config nor the bundled example could be read. The assets/config/ folder is damaged or incomplete in this build. Restore app_config.example.json from the release, copy it to app_config.json, set your values and build again.

Confirm. dart run tool/apply_app_config.dart --check --release exits without errors. It applies the app's own rules to baseUrl, wsBaseUrl, appName, the identifiers and the colours, so none of those will stop the app. It does not check the type of the other keys, such as googleAuthEnabled or settingsCacheDuration: for those, a debug run that gets past this screen is the test.

The certificate is refused, and errors read "Unexpected error occurred"

Symptom. Screens show Unexpected error occurred rather than No internet connection, the market list is empty and no offline banner appears.

Cause. The phone does not trust the server's TLS certificate: it is self-signed, expired, issued for a different host name, or served without its intermediate certificate. The app accepts only certificates the phone trusts, and it has no override. A failed TLS handshake is not reported as a connection problem, so it reaches the screen as the generic message.

Fix. Install a certificate from a public authority for the exact host in baseUrl and wsBaseUrl, with its full chain. See SSL.

Confirm. From a computer, curl -v https://exchange.example.org/api/settings completes the handshake without -k. If curl complains about the certificate, the app hits the same problem.

Prices stay at zero and nothing updates live

The market list loads, but prices and percentage changes stay at zero or never move. Charts and order books stay empty. Everything else that updates live stops updating too: open orders, deposit confirmations, support chat replies and in-app notifications. The announcements slider on Home stays empty.

Cause. The market list is an ordinary HTTPS request. Everything live is a WebSocket to wsBaseUrl plus a path under /api/ (/api/exchange/ticker, /api/exchange/market, /api/ecosystem/ticker, /api/user, and others). A proxy that does not pass WebSocket upgrades on /api/ breaks all of these, while ordinary pages keep working. A second cause is a wsBaseUrl that points at a different or wrong host.

Fix. Give the location /api/ block proxy_http_version 1.1 and the Upgrade and Connection headers, as in nginx: WebSockets. Set wsBaseUrl to wss:// and the same host as baseUrl. If a CDN or firewall sits in front, make sure it passes WebSockets and the headers accesstoken, sessionid, csrftoken and platform. The app uses these headers to sign in its account-specific sockets.

Confirm. The WebSocket curl in Start here prints HTTP/1.1 101 Switching Protocols. A 200, 400 or 502 means the proxy is still not passing the upgrade.

The app logs ✅ TICKER_WS: Connected successfully as soon as it starts opening the socket, before the server has answered. A socket that then fails logs ❌ TICKER_WS: Error - ..., followed by 🔄 TICKER_WS: Reconnecting in Ns (attempt N). It keeps retrying, backing off to once every 30 seconds, for as long as a screen needs prices. Look for these lines, not the first one.

On an install without the Ecosystem add-on, this line is expected and is not a fault: ℹ️ ECO_TICKER_WS: no ecosystem markets feed on this install — not retrying (spot tickers are unaffected).

Coins show letters instead of logos

Symptom. Where the website shows a coin's logo, the app shows a tinted tile with up to three letters of the symbol (BTC).

Cause. The app loads coin art from your server: first the icon the API gives for that coin (a relative path is joined to the host in baseUrl), then the standard /img/crypto/<symbol>.webp on the host in baseUrl. If every address fails, it draws the letters. The art lives in the website's public/img/crypto folder. If baseUrl names a host that reaches only the API (an api. subdomain, or a proxy that forwards only /api), every logo returns 404. Builds before 5.3.7 drew letters for every coin whatever the server, so letters alone do not tell you which cause you have.

Fix. Point baseUrl at the site address, the host whose / serves your website. With the standard nginx layout (/ to the website, /api/ to the backend), that host serves /img/crypto/ for every coin. A single coin shown as letters has no art file under its symbol in public/img/crypto.

Confirm. Open https://<your baseUrl host>/img/crypto/btc.webp in a browser. It must show the image.

Home says "We couldn't check which tools are available on your account."

Symptom. After sign-in, the Trading Tools section on Home shows that sentence with a Retry button. The Trade tab's order form shows Couldn't check what's available on your account. Tabs and tiles for P2P, staking, futures and other modules are missing.

Cause. The app decides which screens exist from one request, GET /api/user/modules, sent after every sign-in (see Module manifest). Until that request succeeds, the app hides every module rather than guess. The request failed: a server error, a core version without the route, or a proxy refusing it.

Fix. Find the failing request in the backend log and fix its cause. Update the core if the route does not exist. The customer can tap Retry. The app also asks again when it returns to the foreground while nothing has loaded. A failed refresh keeps the last list that loaded successfully, so this card means no list has loaded at all.

Confirm. The backend log shows /api/user/modules answering for that user, and Retry replaces the card with the tiles.

Tiles appear only for modules your server reports as installed, licensed and switched on for that customer. A platform with no add-ons has few tiles.

A screen says it is not offered, or asks for verification

The app shows the server's reason on a locked screen or tile. Each message means something different:

The app says Why Where to act
"Module is not offered in your registered country, because this platform is not licensed to provide it there. The rest of your account is unaffected." Enforce Licence Attestations (Mobile App) is on, and the customer's country has no licence row for this module Licence attestations
"Tell us your country of residence to use Module. Add it to your profile, or complete identity verification." Enforcement is on and the customer has no known country The customer adds a country or completes KYC
"Complete identity verification to use Module." Your KYC levels require a feature this customer's level lacks Your KYC level settings
"Module is not available on this platform." The add-on is disabled, not installed or not licensed Extension Manager
"Module is not available right now." Any other reason the server gave Backend log

The licence gate applies only to the app, so the same customer can use the module on the website. See Licence attestations.

Customers are signed out: "Your session has ended. Please sign in again."

Symptom. A customer is sent back to the sign-in screen with that notice. If the server gave its own reason, the notice shows that reason instead, for example after too many incorrect codes.

Cause. The server no longer has the session. The app signs out only when the server says the session is gone (Authentication Required, Session expired, Session not found and similar). A wrong password, PIN or code does not sign anyone out; the screen where it was typed shows the error. Sessions end when:

  • the customer signed out that session or reset the password, or an admin changed the account's email, role or two-factor setting, or blocked, suspended or deleted it;
  • the session reached JWT_REFRESH_EXPIRY (default 14d) since sign-in. Access tokens (JWT_EXPIRY, default 15m) renew on their own, and customers never see that;
  • Redis lost the session keys (a restart without persistence, a flush, or eviction under memory pressure). This signs out every customer at once;
  • APP_REFRESH_TOKEN_SECRET changed. No session can renew its access token any more, so every customer is signed out once their access token expires. A changed APP_ACCESS_TOKEN_SECRET alone signs nobody out: the next renewal issues a token under the new secret.

Fix. Keep Redis persistent and give it enough memory that it does not evict keys. Do not rotate APP_REFRESH_TOKEN_SECRET on a live install. Everything else on the list above is working as intended.

Confirm. If many customers are signed out at the same moment, look at Redis (uptime, evicted_keys) and at recent changes to .env. If one customer is signed out, look at that account's recent activity.

Builds before 5.3.5 had no handling for a session the server had ended: the app stayed "signed in" and every screen failed with messages such as Failed to get wallets: Unexpected error: Authentication Required: Session not found. The customer had to sign out by hand.

Push notifications never arrive

Push needs a Firebase project configured in the app and on the server. Each of the following, when missing, stops every push without showing an error.

  1. The Android build has Firebase. android/app/google-services.json is present when you build. A --verbose build log then says google-services.json found - Firebase push ENABLED. Without the file it says google-services.json not found - Firebase push DISABLED. ..., and the app runs with push switched off.

  2. The iOS build has Firebase and the push capability. ios/Runner/GoogleService-Info.plist is added to the Runner target, the Runner target has the Push Notifications capability (Xcode → Runner → Signing & Capabilities → + Capability), and an APNs key is uploaded in the Firebase console. The project ships without the capability. Without it the app still asks and the customer still agrees, but nothing is delivered. The preflight reports this as ios-push.

  3. The server can send. FCM_PROJECT_ID, FCM_CLIENT_EMAIL and FCM_PRIVATE_KEY (or FCM_SERVICE_ACCOUNT_PATH) are set in .env from a service account of the same Firebase project as the app's files. The backend log says why it disabled FCM (Missing FCM_PROJECT_ID, ... still holds the example value shipped in .env.example — FCM disabled., FCM_PRIVATE_KEY is not a readable PEM). See Notifications: push.

  4. The customer allowed notifications. Shortly after the first sign-in on an install, when the phone does not already allow them, the app shows its own Stay informed sheet and then the phone's prompt. It asks once per install and never again by itself, whatever the answer. A customer who chose Not now or declined receives nothing until they allow notifications for the app in the phone's settings; the app registers the phone at the next sign-in or the next time it starts.

  5. The customer has not switched push off. The app's registration switches push on for a customer who has never set the preference, and never for one who turned it off. Push Notifications under Profile → Notifications in the app must be on (the customer taps Save after changing it).

A signed-in app user with notifications allowed receives pushes without turning anything else on. That needs Core 6.6.3 or later, whose device registry does the switching on. Builds up to 5.3.9 are the exception: see Problems fixed in a later version.

Two behaviours are by design. A push tapped while the app is closed or in the background opens the app where it was, not a particular screen. The admin Notification Service screen names vapid (Web Push) as the provider whenever web push keys are set, even when FCM is also working.

Confirm. Sign in on a phone, allow notifications, then trigger an event that notifies (a completed deposit, a P2P trade update). With no push, work through the steps above in order.

Sign-up or "Forgot Password?" says the security check "can only be completed on the website"

The full messages are:

  • Sign-up: This server's security check can only be completed on the website. Please create your account on the website, then sign in here.
  • Forgot Password? and Change Password (which goes through the same reset): This server's security check can only be completed on the website. Please reset your password on the website, then sign in here with your new password.

Cause. Your server's captcha is a hosted provider: Cloudflare Turnstile, Google reCAPTCHA or hCaptcha. The app answers only the built-in proof-of-work captcha, so a hosted provider's check refuses it. Sign-in from the app does not go through the captcha and keeps working. If the hosted provider has no secret key, sign-up fails with Registration is temporarily unavailable. Please try again shortly. instead, because registration refuses whenever the captcha cannot be checked; a password reset goes through in that case. On the Reset Password screen the message stays in a box above Send Reset Link; on sign-up it is a red message at the bottom of the screen.

Fix. Choose one:

  • Set Admin → System → Platform Settings → Security → Protection → Captcha Provider to Proof of Work (built-in, no keys) (the default) or None — no captcha at all. See Bot protection.
  • Keep the hosted provider and send customers to your website to sign up and reset passwords.

Confirm. https://exchange.example.org/api/auth/pow/challenge?action=register reports "provider":"pow" (or "none").

Builds up to 5.3.9 behave differently. They refuse Forgot Password? and Change Password even under proof-of-work, with Security verification failed. Please try again. A server set to None lets those builds reset a password; otherwise customers on them reset on the website. See Problems fixed in a later version.

Google sign-in fails

The app shows Cause Fix
Failed to get Google ID token. Google returned no ID token: usually googleServerClientId is empty, or the Android OAuth client for your package name and signing key is missing Set googleServerClientId to your OAuth Web client ID, and register the Android client in the same Google Cloud project
An error starting Google authentication failed: The ID token was issued for a client other than the one your server checks Make googleServerClientId in the app equal to NEXT_PUBLIC_GOOGLE_CLIENT_ID on the server
This Google account is not linked. Please log in with your password first and link Google from your profile. The app's Google button only signs in accounts already linked to Google. It cannot create an account The customer does what the message says: signs in with the password and links Google from the profile first, or registers with email
Unexpected error during Google sign in for every linked account A build up to 5.3.9. Those builds misread the server's answer to a successful Google sign-in, and to its request for a two-factor code, so no Google sign-in ever completed Build and publish 5.4.0 or later

A Google account whose owner has two-factor on goes to the same Verification Required screen as a password sign-in. Each tap on the button shows the Google account picker, so a customer can switch accounts.

Whether the button appears is decided by the app's own config, not by the server's Google OAuth Login switch: googleAuthEnabled when it is set, and when that key is absent, whether googleServerClientId is set. On Android the client ID can come from google-services.json instead; if you rely on that and leave googleServerClientId empty, set "googleAuthEnabled": true, or the button stays hidden. See Sign-in methods.

Sign-in says "2FA response is missing authentication tokens"

Cause. The server did not recognise the request as coming from the app, so it returned its sign-in tokens as browser cookies instead of in the reply. The app identifies itself with the request header platform: mobile. Something between the phone and the backend removed it.

Fix. Make the proxy, CDN or firewall pass the platform, accesstoken, sessionid and csrftoken headers through unchanged. A standard nginx location /api/ block passes them.

Figures on the website are missing in the app

APR and APY, projected returns and maximum leverage appear on the website but not in the app. This is by design. The server removes forward-looking figures from its replies to the app, and removes unsafe HTML from article text, because app stores treat promised returns as a policy problem. Realised profit and loss and earned amounts are kept.

The name or the ID on the phone did not change

The name, the Android application ID, the iOS bundle ID and the colours all come from assets/config/app_config.json. The icon comes from assets/icons/app_icon.png. See Branding.

  1. Edit the right file and build again. Change app_config.json, not app_config.example.json. The home-screen name and both IDs are applied when the app is built, so a build made before your change keeps the old values. You do not need flutter clean.

  2. iOS bundle ID: run the apply tool. After you set or change iosBundleId, run dart run tool/apply_app_config.dart. It writes the ID into the Xcode project. If you skip it, every iOS build stops at the Apply app_config.json phase with a message that the config and the project disagree.

  3. Icon: regenerate. Replacing app_icon.png changes nothing by itself. Run dart run tool/apply_app_config.dart --icons.

  4. Look for leftovers from an older release. Run dart run tool/release_preflight.dart --explain. It names each file that still overrides the config:

    • android-label-hardcoded: AndroidManifest.xml has a typed-in android:label. Older installers wrote the name there. Change it back to android:label="${appName}".
    • android-label-placeholder-missing: the Android build fails with Manifest merger failed ... no value for <appName> is provided. Your build.gradle.kts is older than your manifest. Take this release's android/app/build.gradle.kts, after copying any applicationId from your old file into androidApplicationId.
    • android-id-hardcoded: build.gradle.kts has a typed-in applicationId, which wins over the config.
    • ios-name-phase-missing: the Xcode project has no Apply app_config.json build phase, so iOS shows whatever Info.plist says.
  5. Clear the launcher's memory. If the name inside the app is right but the home screen is not, some launchers keep the old label. Uninstall the app and install it again.

Google Play accepts an update only under the ID it already knows. A build with a new androidApplicationId installs next to the old app and cannot be uploaded to the old listing. Before a release, put the ID you have already published into androidApplicationId. Otherwise the build uses the default, com.bicrypto.mobile. The same is true of iosBundleId on the App Store.

The build fails

Match the message:

pub get fails to resolve versions, or the SDK is "too old"

The shipped pubspec.lock needs Flutter 3.38 or newer (Dart 3.10 or newer), even though pubspec.yaml says only sdk: ^3.5.0. Run flutter --version. Install a current stable Flutter, then run flutter pub get again.

"flutter.sdk not set in local.properties"

android/local.properties is written by Flutter and does not ship. Run flutter pub get in the project root before opening android/ in Android Studio or running Gradle yourself.

The Android build asks for SDK Platform 36 or NDK 27.0.12077973

compileSdk, targetSdk (both 36) and ndkVersion (27.0.12077973) are fixed in android/app/build.gradle.kts. They do not follow the installed Flutter. When the build stops naming one of them, install Android SDK Platform 36 and NDK (Side by side) 27.0.12077973 in Android Studio's SDK Manager, and accept the licences with flutter doctor --android-licenses. Do not lower the numbers: Google Play requires target API 36.

Gradle says it needs a newer Java

Gradle and the Android Gradle Plugin (8.11.1) must run on JDK 17 or newer. The app itself compiles for Java 11, which is a separate setting. Point Flutter at a JDK 17 or 21 with flutter config --jdk-dir <path>, or use the JDK that ships with Android Studio.

The build machine cannot spare 8 GB for Gradle

android/gradle.properties lets Gradle's heap grow to 8 GB (org.gradle.jvmargs=-Xmx8G ...). On a build machine that cannot give Gradle that much memory, lower -Xmx.

"No matching client found for package name"

android/app/google-services.json was downloaded for a different application ID. In the Firebase console, add an Android app with your androidApplicationId, download its google-services.json into android/app/, and build again.

Gradle stops on app_config.json

Gradle reads assets/config/app_config.json on every Android build and stops on a file the app would refuse: ... is not valid JSON (at line N column M), an appName containing a line break or tab ("appName" contains a control character ...), or an androidApplicationId that is not a valid ID (... which is not a valid Android application ID). Fix the value it names. dart run tool/apply_app_config.dart --check reports all of them at once.

A Gradle error about key.properties that names no key

android/key.properties exists but lacks one of its four lines: storeFile, storePassword, keyAlias, keyPassword. Gradle fails with an error that names none of them. The preflight names the missing one (release-signing). A relative storeFile is resolved from android/app/, so use an absolute path.

"The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation."

The zip ships without ios/Pods and Podfile.lock. Run flutter pub get, then cd ios && pod install, and open ios/Runner.xcworkspace (not the .xcodeproj) in Xcode. flutter build ios runs the pod step for you.

"... Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"

You ran pod install before flutter pub get. Run flutter pub get in the project root, then pod install again.

The iOS build stops at "Apply app_config.json"

iosBundleId in app_config.json and the bundle ID in the Xcode project differ. If the config is right, run dart run tool/apply_app_config.dart. If the project is right, put its ID into iosBundleId, or set iosBundleId to "" to skip the check. To see the result without a Mac: dart tool/ios_app_identity.dart --dry-run.

Xcode cannot sign the Runner target

Signing needs your Apple team. Set iosTeamId in app_config.json to your 10-character Team ID and run dart run tool/apply_app_config.dart, or choose your Team under Runner → Signing & Capabilities. The bundle ID must match an App ID registered to that team. iosTeamId must be exactly 10 upper-case letters and digits; the tool writes it into every Runner configuration of the Xcode project. This release's project carries no team, so until you set one the preflight reports the empty Release configuration as ios-team. An Xcode project taken from 5.3.9 or earlier still names the vendor's team, 52K7QMD7KR, which the preflight also reports as ios-team. A project whose team differs from a set iosTeamId is ios-team-mismatch. If you build only for Android, run the preflight with --android-only, which skips every iOS check.

A web build does not start

The web is not a target. The app's start-up code asks which phone platform it is running on, with no web fallback, and push, signing and store packaging are built for Android and iOS only. The installers still offer a web option; do not use it.

Without android/key.properties, flutter build appbundle --release signs with the debug key. The only warning Gradle prints is hidden by Flutter's output filter. You get a finished-looking .aab that Play Console refuses. Run the preflight before every upload: it reports this as release-signing.

The store rejected the upload or the app

Play refuses the bundle as debug-signed

See the warning above. Create an upload keystore, write the four lines of android/key.properties, and build again. To check an artifact before uploading:

keytool -printcert -jarfile build/app/outputs/bundle/release/app-release.aab

The owner must be your certificate, not CN=Android Debug.

If a debug-signed build is installed on a test phone, uninstall it before installing a properly signed one. Android does not update an app with a build signed by a different key.

The store refuses the build number

Both stores refuse a build number they have already received. The number comes from the +N part of version: in pubspec.yaml (5.3.9+10), or from --build-number. Raise it for every upload. A new release zip carries the vendor's number, which can be lower than one you have already uploaded. See Updating your app.

Target API level

Google Play requires target API 36 for new apps and for updates. The project fixes compileSdk and targetSdk at 36 so an older Flutter cannot quietly lower them. Do not change them. The preflight reports a lower value as android-target-api.

Account deletion

Both stores require in-app account deletion. The app has it: the gear icon on Home → Delete Account. Play also asks for a web address; use your site's public /account-deletion page.

Deletion in the app asks for the account's current password. An account created on the website with Google has no password, so its owner cannot finish deletion in the app, and the website has no deletion control. The /account-deletion page sends anyone who cannot delete in the app to support, from the email address the account uses. Handle those requests through support. The app itself never creates a password-less account, because its Google button only signs in accounts already linked to Google. Give the store reviewer an account with a password.

Restricted financial products

Merchant, AI Investment and the affiliate programme are compiled into a build only when you build with --dart-define=STORE_RESTRICTED_MODULES=true, and AI Investment and the affiliate programme also need the server switch Allow AI Investment & MLM in the Mobile App. Store policies on financial products and investment schemes make these the likeliest cause of a rejection. If a review names them, build without the flag. Binary options, forex investment plans and creating a token offering are never available in the app, whatever the switches. See Restricted modules.

The reviewer could not sign in, or saw empty screens

Give App Review a real, verified account whose KYC level allows everything the reviewer will try. If licence attestations are enforced, that account also needs a residence with licence rows. See App review access.

"Created from a template" (Apple 4.2.6, 4.3(b))

Run dart run tool/release_preflight.dart --explain and settle every item. See Differentiation.

AI Market Maker refuses to start

Symptom. The AI Market Maker stays stopped. The backend log says Engine refused to start: this platform ships a mobile app (mobileAppEnabled). ...

Cause. This platform ships a mobile app is switched on in the AI Market Maker settings (Status). An app store treats synthetic order-book depth as deceptive, so the market maker and a published app cannot run on one platform. No permission overrides this.

Fix. None while you publish the app. Turn the switch off only if you do not publish an app.

Problems fixed in a later version

A customer on an old build keeps the old behaviour until they install your update. Nothing on the server forces an update. Check the customer's version first (see Start here).

Symptom Builds affected Fixed in Also needs
"Signed in", but every screen fails with Authentication Required: Session not found Before 5.3.5 5.3.5 —
Spot deposit on a network the platform cannot hold spins for ever Before 5.3.4 5.3.4 —
Ecosystem (own-chain) coins show no price, 0.00% and an empty chart Before 5.3.6 5.3.6 Core 6.8.2 for markets with no trades yet; WebSocket upgrades through the proxy
Home portfolio card shows a fixed figure instead of the customer's total Before 5.3.6 5.3.6 —
Action buttons (P2P offer wizard, checkout Complete Order) hidden under the phone's navigation bar Before 5.3.7 5.3.7 —
Every coin drawn as letters; staking logos never load Before 5.3.7 5.3.7 baseUrl set to the site address
Two-factor setup refuses every correct code with Invalid OTP provided Before 5.3.8 5.3.8 —
The News tile opens the blog; added news sources never show; Load more does not grow the list Before 5.3.9 5.3.9 Core 6.8.2 for the Market News tile
The version the app reports is wrong (a 5.3.8 build says 5.3.7) Before 5.3.9 5.3.9 —
A mistyped code, PIN or password (for example a seller's escrow-release code) signs the customer out 5.3.5 to 5.3.9 5.4.0 —
The two-factor screen at sign-in accepts only six-digit codes, not recovery codes Up to 5.3.9 5.4.0 —
"Forgot Password?" and "Change Password" refused with Security verification failed. Please try again. under the default proof-of-work captcha Up to 5.3.9 5.4.0 —
After Change Password, signing in again says An error occurred. Please restart the app. until the app is closed fully Up to 5.3.9 5.4.0 —
Every Google sign-in of a linked account fails with Unexpected error during Google sign in Up to 5.3.9 5.4.0 —
The Google button shows with no googleServerClientId set and answers every tap with Failed to get Google ID token. Up to 5.3.9, when googleAuthEnabled is absent 5.4.0 —
Pushes arrive only after the customer turns on Push Notifications on the app's Notifications screen Up to 5.3.9 5.4.0 Core 6.6.3 or later
The app asks the phone for notification permission at every sign-in and every start with a saved sign-in, instead of once Up to 5.3.9 5.4.0 —
The Store cart shows shipping of 9.99, free from 50.00, and no tax, whatever your E-commerce settings, so its total differs from what checkout charges Up to 5.3.9 5.4.0 —
With defaultExchangeProvider set to xt, the chart offers Binance's timeframes instead of XT's Up to 5.3.9 5.4.0 —
A release build accepts an http:// or ws:// address Up to 5.3.9 5.4.0 —

Not faults: what the app does not do

Customers and staff ask about these as if they were bugs.

  • No links open the app. Email verification and password-reset links open your website. A website link cannot open the app. A push tapped while the app is closed opens the app where it was left.
  • English only. The app has no language picker and follows no phone language.
  • No crash reporting or analytics. Nothing reports crashes to you. Use the crash reports in Google Play Console and App Store Connect.
  • No forced updates. The admin settings Minimum iOS App Version and Minimum Android App Version are not read by the app. Old builds keep working until customers update from the store, so keep your server compatible with them.
  • No administration in the app. Staff accounts use the app like customers. Every admin screen stays on the website.
  • Configuration changes need a new build. baseUrl, colours, the name and every other config value are packed into the build. Moving your backend to a new domain means a new build, a store update, and keeping the old host answering until customers have updated.