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.
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.jsonchanges 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
baseUrlandwsBaseUrl. A proxy that serves the website correctly can still break the app.
Start here: three checks
-
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): theversion:line ofpubspec.yaml, asname (build number). Builds before 5.3.9 showed the hand-typedappVersionvalue from the config instead, which could be wrong. -
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 --explainThe first command changes nothing. It lists every invalid value in
app_config.jsonat 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 abaseUrlthat is still a placeholder (backend-url), and anhttp://orws://address (backend-not-encrypted). If you build only for Android, add--android-onlyto skip the iOS checks. -
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 -1Use the exact hosts in your
baseUrlandwsBaseUrl.
"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(default14d) since sign-in. Access tokens (JWT_EXPIRY, default15m) 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_SECRETchanged. No session can renew its access token any more, so every customer is signed out once their access token expires. A changedAPP_ACCESS_TOKEN_SECRETalone 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.
-
The Android build has Firebase.
android/app/google-services.jsonis present when you build. A--verbosebuild log then saysgoogle-services.json found - Firebase push ENABLED. Without the file it saysgoogle-services.json not found - Firebase push DISABLED. ..., and the app runs with push switched off. -
The iOS build has Firebase and the push capability.
ios/Runner/GoogleService-Info.plistis 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 asios-push. -
The server can send.
FCM_PROJECT_ID,FCM_CLIENT_EMAILandFCM_PRIVATE_KEY(orFCM_SERVICE_ACCOUNT_PATH) are set in.envfrom 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. -
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.
-
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.
-
Edit the right file and build again. Change
app_config.json, notapp_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 needflutter clean. -
iOS bundle ID: run the apply tool. After you set or change
iosBundleId, rundart 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. -
Icon: regenerate. Replacing
app_icon.pngchanges nothing by itself. Rundart run tool/apply_app_config.dart --icons. -
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.xmlhas a typed-inandroid:label. Older installers wrote the name there. Change it back toandroid:label="${appName}".android-label-placeholder-missing: the Android build fails withManifest merger failed ... no value for <appName> is provided. Yourbuild.gradle.ktsis older than your manifest. Take this release'sandroid/app/build.gradle.kts, after copying anyapplicationIdfrom your old file intoandroidApplicationId.android-id-hardcoded:build.gradle.ktshas a typed-inapplicationId, which wins over the config.ios-name-phase-missing: the Xcode project has no Apply app_config.json build phase, so iOS shows whateverInfo.plistsays.
-
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.aabThe 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.