chore(android-terminal): dev cleartext + readable HTTP error messages
Some checks failed
Some checks failed
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) <noreply@anthropic.com>
This commit is contained in:
10
clients/terminal-android/app/src/debug/AndroidManifest.xml
Normal file
10
clients/terminal-android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Debug-only manifest overlay. Merged onto the main manifest by AGP for
|
||||
debug builds; release builds skip this and keep the platform default
|
||||
(no cleartext HTTP). -->
|
||||
<application
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:replace="android:networkSecurityConfig" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Debug-only: allow cleartext HTTP to the dev server. The release build
|
||||
(in app/src/main/AndroidManifest.xml) does NOT include this resource and
|
||||
keeps the platform default which forbids cleartext entirely.
|
||||
|
||||
10.0.2.2 → host machine from a standard Android emulator
|
||||
localhost → loopback within the emulator itself (rarely useful)
|
||||
Add your LAN IP here too if you test against a hardware tablet on wifi.
|
||||
-->
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user