Report invitation presentation errors without exposing link secrets

This commit is contained in:
evgeny committed 2026-10-28 12:28:28 +01:00
1 parent 6f9ddc88e2
commit 237ba93e1a
1 file changed
+38 -9
@@ -36,6 +36,11 @@ private fun candidateLabel(c: InviteCandidate): String =
else -> ""
}
/** Показ зарегистрированной ссылки/QR и отправка её устройству в LAN; генерацией и корреляцией link/pending владеет caller.
* Первый candidate — визуальный fallback; явный выбор вызывает onSelectNode, начальный запрос делает caller.
* При открытии запрашивает LAN-снимок и снимает только прежний терминальный результат; активная отправка переживает закрытие.
* Результат LAN относится к сохранённому адресату, не означает JOIN. Состояние копирования сбрасывается при смене ссылки.
* Ошибки QR оставляют текст/копирование доступными; ошибки clipboard показываются в диалоге. Содержимое ссылки не логируется. */
@Composable
fun InviteLinkDialog(
candidates: List<InviteCandidate>,
@@ -45,7 +50,8 @@ fun InviteLinkDialog(
onDismiss: () -> Unit
) {
val context = LocalContext.current
var copied by remember { mutableStateOf(false) }
var copied by remember(link) { mutableStateOf(false) }
var copyFailed by remember(link) { mutableStateOf(false) }
var selectedNodeId by remember { mutableStateOf<Long?>(null) }
var menuOpen by remember { mutableStateOf(false) }
val devices by LocalDiscovery.devices.collectAsState()
@@ -53,7 +59,14 @@ fun InviteLinkDialog(
var selectedDevice by remember { mutableStateOf<Long?>(null) }
var devicesOpen by remember { mutableStateOf(false) }
val device = devices.firstOrNull { it.nodeId == selectedDevice } ?: devices.firstOrNull()
LaunchedEffect(Unit) { LocalDiscovery.clearDelivery(); NativeLib.requestLocalDevices() }
LaunchedEffect(Unit) {
LocalDiscovery.clearDelivery()
try {
if (!NativeLib.requestLocalDevices()) LogManager.addLog("WARN", "INVITE", "LAN device snapshot request rejected")
} catch (error: Exception) {
LogManager.addLog("ERROR", "INVITE", "LAN device snapshot request failed: ${error.javaClass.simpleName}")
}
}
val effectiveSelected = remember(candidates, selectedNodeId) {
candidates.firstOrNull { it.nodeId == selectedNodeId } ?: candidates.firstOrNull()
@@ -110,6 +123,7 @@ fun InviteLinkDialog(
}
else -> Text(stringResource(R.string.invite_build_failed), fontFamily = FontFamily.Monospace, fontSize = 11.sp)
}
if (copyFailed) Text(stringResource(R.string.invite_copy_failed), color = MaterialTheme.colorScheme.error)
if (devices.isNotEmpty()) {
Spacer(Modifier.height(8.dp))
Text(stringResource(R.string.local_invite_devices), style = MaterialTheme.typography.bodyMedium)
@@ -128,24 +142,37 @@ fun InviteLinkDialog(
}) { Text(stringResource(R.string.local_invite_send)) }
}
delivery?.let { status ->
Text(stringResource(when {
val text = stringResource(when {
status.pending -> R.string.local_invite_sending
status.forbidden -> R.string.local_invite_forbidden
status.received -> R.string.local_invite_delivered
else -> R.string.local_invite_failed
}), style = MaterialTheme.typography.bodySmall)
})
Text(stringResource(R.string.invite_delivery_status, status.device.label, text),
style = MaterialTheme.typography.bodySmall)
}
}
},
confirmButton = {
Button(
enabled = link != null,
enabled = link != null && !pending,
onClick = {
link?.let {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("invite", it))
copied = true
LogManager.addLog("INFO", "INVITE", "Link copied to clipboard")
copyFailed = false; copied = false
try {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
if (clipboard == null) {
copyFailed = true
LogManager.addLog("ERROR", "INVITE", "Clipboard service unavailable")
} else {
clipboard.setPrimaryClip(ClipData.newPlainText("invite", it))
copied = true
LogManager.addLog("INFO", "INVITE", "Link copied to clipboard")
}
} catch (error: Exception) {
copyFailed = true
LogManager.addLog("ERROR", "INVITE", "Clipboard copy failed: ${error.javaClass.simpleName}")
}
}
}
) { Text(stringResource(if (copied) R.string.copied else R.string.copy_link)) }
@@ -156,6 +183,7 @@ fun InviteLinkDialog(
)
}
/** QR 400×400 для текущей ссылки; ошибка логируется без секретных данных и оставляет владельцу текстовый fallback. */
private fun generateQRCode(text: String): Bitmap? {
return try {
val writer = MultiFormatWriter()
@@ -168,6 +196,7 @@ private fun generateQRCode(text: String): Bitmap? {
}
bitmap
} catch (e: Exception) {
LogManager.addLog("ERROR", "INVITE", "QR generation failed length=${text.length}: ${e.javaClass.simpleName}")
null
}
}