From 165847da1ca3c3be627b5968bc77e9a11b3385cb Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 17 Oct 2026 13:29:25 +0200 Subject: feat(android): ask for the local network when a group opens Android 17 makes it a permission for an application targeting SDK 37. Co-Authored-By: Claude Opus 5.5 --- .../src/meshbay_hub/static/group-page.js | 46 +++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 728ffc37..45049a16 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -31,6 +31,10 @@ import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js'; // each try is a connection. const DEVICE_RETRY_MS = 5000; +// Whether this application has asked for the local network yet in this run: +// once, when the first group is opened, and again only when the person says so. +let lanAsked = false; + /** The node's `expires_at` (ISO) in epoch seconds; 0 when it gave none. */ function _untilSeconds(iso) { const ms = Date.parse(iso || ''); @@ -240,6 +244,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, const [passInput, setPassInput] = useState(''); const [passBusy, setPassBusy] = useState(false); const [retryKey, setRetryKey] = useState(0); + // In the Android application, when the local network was refused: whether + // Android will still ask ("prompt") or only its settings can change it. + const [lanBlocked, setLanBlocked] = useState(''); const transportRef = useRef(null); const gekRef = useRef(null); // One refresh per group: if a fresh token still says we are not a member, we @@ -398,6 +405,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, const connect = async () => { setStatus('discovering'); setError(''); + setLanBlocked(''); // Belongs to the connection about to be made, not the group just left. setDeviceReady(false); gekRef.current = null; @@ -425,6 +433,16 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // from the node, or creates one there on a first join. const identity = null; + // Android 17 asks before this application reaches the local network, + // where the node usually is: when a group is first opened rather than + // at start, and a refusal is explained below if it is why the group + // does not open (local_network_blocked). + if (platform.localNetwork.available && !lanAsked) { + lanAsked = true; + try { await platform.localNetwork.request(); } catch { /* the connection says the rest */ } + if (cancelled) return; + } + setStatus('connecting'); // Renewed here rather than taken from the prop. This effect no longer // re-runs when the token rotates (see the dependency list below), so @@ -706,7 +724,14 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // This device dropped the node's local addresses (transport.js // localNetworkRefused): a setting to change here, not a node to wait // for. Android 17 gates it per application, Chrome from 155. - if (err.reason === 'local_network_blocked') { + if (err.reason === 'local_network_blocked' && platform.localNetwork.available) { + err.message = t('group.lan_blocked_app'); + try { + const state = await platform.localNetwork.state(); + if (cancelled) return; + setLanBlocked(state === 'granted' ? '' : state); + } catch { /* the message stands alone */ } + } else if (err.reason === 'local_network_blocked') { err.message = t(/Android/i.test(navigator.userAgent) ? 'group.lan_blocked_android' : 'group.lan_blocked_browser'); } @@ -1038,6 +1063,25 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, `} + ${lanBlocked === 'prompt' && html` + `} + ${lanBlocked === 'denied' && html` +

${t('group.lan_settings_hint')}

+ `} ${needsDevice && nativeKeys && html`

${t('device.add_title')}

-- cgit v1.2.3