Merge pull request 'fix(wallet+companion): pre-demo blockers — Send modal TDZ crash, cold-start inset collapse, loader branding — 0.5.10' (#118) from fix/safe-area-insets into main
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m47s
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m47s
This commit is contained in:
commit
9a66f22138
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.archipelago.app"
|
applicationId = "com.archipelago.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 29
|
versionCode = 30
|
||||||
versionName = "0.5.9"
|
versionName = "0.5.10"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.archipelago.app.ui.components
|
package com.archipelago.app.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -12,6 +13,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
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))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "F*CK IPs MESH",
|
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]. */
|
/** True when a TCP listener answers at [base]'s host:port within [timeoutMs]. */
|
||||||
private fun tcpAnswers(base: String, timeoutMs: Int): Boolean = try {
|
private fun tcpAnswers(base: String, timeoutMs: Int): Boolean = try {
|
||||||
val u = android.net.Uri.parse(base)
|
val u = android.net.Uri.parse(base)
|
||||||
@ -429,6 +457,14 @@ fun WebViewScreen(
|
|||||||
|
|
||||||
val webViewRef = this
|
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:
|
// Decide where an outbound URL goes:
|
||||||
// - same host as the node → in-app WebView overlay
|
// - same host as the node → in-app WebView overlay
|
||||||
// (this is the "open in browser" target for apps the
|
// (this is the "open in browser" target for apps the
|
||||||
@ -515,34 +551,7 @@ fun WebViewScreen(
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
if (view == null) return
|
if (view == null) return
|
||||||
|
|
||||||
// Convert physical pixels → CSS pixels
|
injectSafeAreaVars(view)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Auto-login with the stored password (QR pairing /
|
// Auto-login with the stored password (QR pairing /
|
||||||
// saved server) — only on our own server's pages
|
// saved server) — only on our own server's pages
|
||||||
@ -733,10 +742,12 @@ fun WebViewScreen(
|
|||||||
} else {
|
} else {
|
||||||
// Reattached views keep stale measurements until an
|
// Reattached views keep stale measurements until an
|
||||||
// input event — that was the top/bottom UI being
|
// 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 {
|
post {
|
||||||
requestLayout()
|
requestLayout()
|
||||||
invalidate()
|
invalidate()
|
||||||
|
injectSafeAreaVars(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@ -193,22 +193,6 @@ function toggleSendAll() {
|
|||||||
// Leaving the on-chain tab disarms the sweep so it can never apply elsewhere
|
// Leaving the on-chain tab disarms the sweep so it can never apply elsewhere
|
||||||
watch(sendMethod, (m) => { if (m !== 'onchain') sendAll.value = false })
|
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<number | null>(() => {
|
|
||||||
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
|
// 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.
|
// button where it can't work — the textarea still accepts a manual paste.
|
||||||
const canReadClipboard = typeof navigator !== 'undefined' && !!navigator.clipboard?.readText
|
const canReadClipboard = typeof navigator !== 'undefined' && !!navigator.clipboard?.readText
|
||||||
@ -231,6 +215,25 @@ const effectiveMethod = computed(() => {
|
|||||||
return 'lightning'
|
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<number | null>(() => {
|
||||||
|
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
|
// --- Second-step confirmation (parity with the scan flow): review shows the
|
||||||
// --- balance reduction before anything is sent or any token is minted.
|
// --- balance reduction before anything is sent or any token is minted.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user