From c158d920d21f183709c10c8c79abb36076c7d848 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Wed, 6 May 2026 23:17:44 +0200 Subject: [PATCH] chore(android-terminal): dev cleartext + readable HTTP error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small dev-quality fixes shaken out by manual testing: - Add a debug-only network_security_config.xml that whitelists 10.0.2.2, localhost and 127.0.0.1 for cleartext HTTP. Without this, the dev emulator can't reach the Python dev server because targetSdk 35 forbids cleartext HTTP by default. Lives under app/src/debug/ so it ships only in debug APKs — release builds keep the platform default (no cleartext at all). - TerminalViewModel.runAction now extracts the JSON {message: ...} field from HttpException response bodies instead of just showing "HTTP 400". Cashiers (and developers) now see "Staff PIN is required for this operation" / "Daily stamp limit of N reached" inline in the failed action sheet, surfacing the same business-error text the server already returns. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/src/debug/AndroidManifest.xml | 10 ++++++++ .../debug/res/xml/network_security_config.xml | 17 +++++++++++++ .../terminal/ui/terminal/TerminalViewModel.kt | 24 ++++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 clients/terminal-android/app/src/debug/AndroidManifest.xml create mode 100644 clients/terminal-android/app/src/debug/res/xml/network_security_config.xml diff --git a/clients/terminal-android/app/src/debug/AndroidManifest.xml b/clients/terminal-android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..4ca45d05 --- /dev/null +++ b/clients/terminal-android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/clients/terminal-android/app/src/debug/res/xml/network_security_config.xml b/clients/terminal-android/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 00000000..92185cb8 --- /dev/null +++ b/clients/terminal-android/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,17 @@ + + + + + 10.0.2.2 + localhost + 127.0.0.1 + + diff --git a/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalViewModel.kt b/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalViewModel.kt index a67d3f03..04d09e49 100644 --- a/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalViewModel.kt +++ b/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalViewModel.kt @@ -25,6 +25,7 @@ import lu.rewardflow.terminal.data.model.TransactionItem import lu.rewardflow.terminal.data.network.NetworkMonitor import lu.rewardflow.terminal.data.repository.CategoryRepository import lu.rewardflow.terminal.data.repository.DeviceConfigRepository +import retrofit2.HttpException import javax.inject.Inject /** @@ -294,13 +295,34 @@ class TerminalViewModel @Inject constructor( _state.value = _state.value.copy( actionInProgress = false, actionResult = ActionResult.Failure( - result.exceptionOrNull()?.message ?: "Operation failed" + readableErrorMessage(result.exceptionOrNull()) ), ) } } } + /** Extract a human-readable message from a Retrofit/HttpException. + * + * Retrofit's default `message` is the HTTP status text ("Bad Request"), + * which hides the JSON ``{message, error_code, details}`` body the + * backend returns. Read the body and surface its message instead so + * cashiers see "Staff PIN is required" or "Card not found" rather + * than the generic 400. */ + private fun readableErrorMessage(t: Throwable?): String { + if (t is HttpException) { + val body = runCatching { t.response()?.errorBody()?.string() }.getOrNull() + if (!body.isNullOrBlank()) { + // Cheap JSON peek — avoid pulling in a full adapter for one field. + val match = "\"message\"\\s*:\\s*\"([^\"]+)\"".toRegex().find(body) + if (match != null) return match.groupValues[1] + return body.take(200) + } + return "HTTP ${t.code()}" + } + return t?.message ?: "Operation failed" + } + fun refreshCurrentCustomer() { val current = _state.value.customer ?: return viewModelScope.launch {