fix(wallet+companion): pre-demo blockers — Send modal TDZ crash, cold-start inset collapse, loader branding — 0.5.10

- SendBitcoinModal crashed AT MOUNT on device ('Cannot access R before
  initialization'): the invoice-first block's watch() evaluates its
  computed source at setup, which read effectiveMethod while still in
  its temporal dead zone (declared 20 lines later). Send button did
  nothing and the failure poisoned sibling modal data. Block moved below
  effectiveMethod, with a comment so it can't drift back.
- Cold start injected 0px safe-area vars (onPageFinished can beat window
  attachment; rootWindowInsets null) — UI lost top/bottom margins and
  the tab bar sat in the gesture zone eating taps. Injection extracted
  to injectSafeAreaVars(), re-fired from an OnApplyWindowInsetsListener
  when real insets arrive, on page finish, and on reattach.
- MeshLoadingScreen wears the proper brand: pixel 'a' in the black
  circle-container + F*CK IPS MESH wordmark.

Served APK: 0.5.10 (vc30). Reproduced+diagnosed live on-device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-07-24 09:03:43 +01:00
co-authored by Claude Fable 5
parent 8af2ca4ac2
commit 504944fd08
5 changed files with 79 additions and 48 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "com.archipelago.app"
minSdk = 26
targetSdk = 35
versionCode = 29
versionName = "0.5.9"
versionCode = 30
versionName = "0.5.10"
vectorDrawables {
useSupportLibrary = true
@@ -1,6 +1,7 @@
package com.archipelago.app.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -12,6 +13,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -37,7 +39,22 @@ fun MeshLoadingScreen(message: String = "Dialing your node by its key — no IPs
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
PixelArtLogo(Modifier.size(96.dp))
// The brand's circle-container logo (as on the connect screen /
// web login): pixel-art "a" centered in a black disc.
Box(
Modifier
.size(120.dp)
.clip(androidx.compose.foundation.shape.CircleShape)
.background(Color.Black)
.border(
1.dp,
Color.White.copy(alpha = 0.14f),
androidx.compose.foundation.shape.CircleShape,
),
contentAlignment = Alignment.Center,
) {
PixelArtLogo(Modifier.size(64.dp))
}
Spacer(Modifier.height(20.dp))
Text(
text = "F*CK IPs MESH",
@@ -150,6 +150,34 @@ private object KioskWebView {
}
}
/** Inject the safe-area CSS vars from the CURRENT window insets. Android
* WebView doesn't populate env(safe-area-inset-*); worse, on a cold start
* onPageFinished can run before the view is attached — rootWindowInsets is
* null then, and injecting 0px collapsed the UI's top/bottom margins (and
* put the tab bar inside the gesture zone, killing its taps). Called from
* onPageFinished, from the window-insets listener (fires when real insets
* arrive), and on reattach. */
private fun injectSafeAreaVars(view: WebView) {
val insets = view.rootWindowInsets ?: return // listener re-fires when real
val density = view.resources.displayMetrics.density
val sat = (insets.getInsets(android.view.WindowInsets.Type.statusBars()).top / density).toInt()
val sab = (insets.getInsets(android.view.WindowInsets.Type.navigationBars()).bottom / density).toInt()
view.evaluateJavascript(
"""
(function() {
var style = document.getElementById('archipelago-android-insets');
if (!style) {
style = document.createElement('style');
style.id = 'archipelago-android-insets';
document.head.appendChild(style);
}
style.textContent = ':root { --safe-area-top: ${sat}px; --safe-area-bottom: ${sab}px; }';
})();
""".trimIndent(),
null,
)
}
/** True when a TCP listener answers at [base]'s host:port within [timeoutMs]. */
private fun tcpAnswers(base: String, timeoutMs: Int): Boolean = try {
val u = android.net.Uri.parse(base)
@@ -429,6 +457,14 @@ fun WebViewScreen(
val webViewRef = this
// Re-inject the safe-area vars whenever REAL insets
// arrive — on cold start onPageFinished often beats
// window attachment and would otherwise bake in 0px.
setOnApplyWindowInsetsListener { v, insets ->
(v as? WebView)?.let { injectSafeAreaVars(it) }
v.onApplyWindowInsets(insets)
}
// Decide where an outbound URL goes:
// - same host as the node → in-app WebView overlay
// (this is the "open in browser" target for apps the
@@ -515,34 +551,7 @@ fun WebViewScreen(
isLoading = false
if (view == null) return
// Convert physical pixels → CSS pixels
val density = view.resources.displayMetrics.density
val satPx = view.rootWindowInsets
?.getInsets(android.view.WindowInsets.Type.statusBars())
?.top ?: 0
val sabPx = view.rootWindowInsets
?.getInsets(android.view.WindowInsets.Type.navigationBars())
?.bottom ?: 0
val sat = (satPx / density).toInt()
val sab = (sabPx / density).toInt()
// Android WebView doesn't populate env(safe-area-inset-*).
// Set CSS custom properties the web UI can use as fallback:
// var(--safe-area-top, env(safe-area-inset-top, 0px))
view.evaluateJavascript(
"""
(function() {
var style = document.getElementById('archipelago-android-insets');
if (!style) {
style = document.createElement('style');
style.id = 'archipelago-android-insets';
document.head.appendChild(style);
}
style.textContent = ':root { --safe-area-top: ${sat}px; --safe-area-bottom: ${sab}px; }';
})();
""".trimIndent(),
null,
)
injectSafeAreaVars(view)
// Auto-login with the stored password (QR pairing /
// saved server) — only on our own server's pages
@@ -733,10 +742,12 @@ fun WebViewScreen(
} else {
// Reattached views keep stale measurements until an
// input event — that was the top/bottom UI being
// wrong until a tap. Force a fresh pass.
// wrong until a tap. Force a fresh pass, and re-sync
// the page's safe-area vars while we're at it.
post {
requestLayout()
invalidate()
injectSafeAreaVars(this)
}
}
}