diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index 47fe25b4..77912acc 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -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 diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/MeshLoadingScreen.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/MeshLoadingScreen.kt index afc15944..aac1270a 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/MeshLoadingScreen.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/MeshLoadingScreen.kt @@ -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", diff --git a/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt b/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt index 9cadc86d..928329b1 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt @@ -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) } } } diff --git a/neode-ui/public/packages/archipelago-companion.apk b/neode-ui/public/packages/archipelago-companion.apk index 42cb1a62..a54e2b77 100644 Binary files a/neode-ui/public/packages/archipelago-companion.apk and b/neode-ui/public/packages/archipelago-companion.apk differ diff --git a/neode-ui/src/components/SendBitcoinModal.vue b/neode-ui/src/components/SendBitcoinModal.vue index f3abe5fa..76d594c7 100644 --- a/neode-ui/src/components/SendBitcoinModal.vue +++ b/neode-ui/src/components/SendBitcoinModal.vue @@ -193,22 +193,6 @@ function toggleSendAll() { // Leaving the on-chain tab disarms the sweep so it can never apply elsewhere watch(sendMethod, (m) => { if (m !== 'onchain') sendAll.value = false }) -// Invoice-first lightning UX: a pasted invoice that fixes its amount locks -// the amount field (auto-filled, "set by invoice"); zero-amount invoices -// leave it editable. Clearing/leaving lightning unlocks again. -const pastedInvoiceAmount = computed(() => { - if (effectiveMethod.value !== 'lightning') return null - const d = dest.value.trim() - if (!d) return null - return parseBolt11AmountSats(d.toLowerCase().startsWith('lightning:') ? d.slice(10) : d) -}) -watch(pastedInvoiceAmount, (fixed, prev) => { - if (fixed !== null) amount.value = fixed - // Swapping a fixed-amount invoice for a zero-amount one: don't silently - // keep the previous invoice's sats — make the user type the new amount. - else if (prev !== null) amount.value = 0 -}) - // Clipboard read needs a secure context (or the companion bridge); hide the // button where it can't work — the textarea still accepts a manual paste. const canReadClipboard = typeof navigator !== 'undefined' && !!navigator.clipboard?.readText @@ -231,6 +215,25 @@ const effectiveMethod = computed(() => { return 'lightning' }) +// Invoice-first lightning UX: a pasted invoice that fixes its amount locks +// the amount field (auto-filled, "set by invoice"); zero-amount invoices +// leave it editable. Clearing/leaving lightning unlocks again. +// MUST come after effectiveMethod: watch() evaluates its source getter at +// setup, and reading a const still in its temporal dead zone crashed the +// whole modal at mount ("Cannot access 'R' before initialization"). +const pastedInvoiceAmount = computed(() => { + if (effectiveMethod.value !== 'lightning') return null + const d = dest.value.trim() + if (!d) return null + return parseBolt11AmountSats(d.toLowerCase().startsWith('lightning:') ? d.slice(10) : d) +}) +watch(pastedInvoiceAmount, (fixed, prev) => { + if (fixed !== null) amount.value = fixed + // Swapping a fixed-amount invoice for a zero-amount one: don't silently + // keep the previous invoice's sats — make the user type the new amount. + else if (prev !== null) amount.value = 0 +}) + // --- Second-step confirmation (parity with the scan flow): review shows the // --- balance reduction before anything is sent or any token is minted.