Skip to content

Commit 3641f55

Browse files
authored
Merge pull request #68 from bobleer/main
feat(remote): update notices
2 parents ef56d70 + 4a71be0 commit 3641f55

9 files changed

Lines changed: 411 additions & 18 deletions

File tree

src/apps/desktop/src/api/remote_connect_api.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
//! Tauri commands for Remote Connect.
22
33
use bitfun_core::service::remote_connect::{
4-
bot::BotConfig, ConnectionMethod, ConnectionResult, PairingState, RemoteConnectConfig,
4+
bot::BotConfig, lan, ConnectionMethod, ConnectionResult, PairingState, RemoteConnectConfig,
55
RemoteConnectService,
66
};
77
use once_cell::sync::OnceCell;
8+
use regex::Regex;
89
use serde::{Deserialize, Serialize};
910
use std::path::PathBuf;
11+
use std::process::Command;
1012
use std::sync::Arc;
1113
use tokio::sync::RwLock;
1214

@@ -220,6 +222,65 @@ pub struct DeviceInfo {
220222
pub mac_address: String,
221223
}
222224

225+
#[derive(Debug, Serialize)]
226+
pub struct LanNetworkInfo {
227+
pub local_ip: String,
228+
pub gateway_ip: Option<String>,
229+
}
230+
231+
fn detect_default_gateway_ip() -> Option<String> {
232+
#[cfg(target_os = "macos")]
233+
{
234+
let output = Command::new("route")
235+
.args(["-n", "get", "default"])
236+
.output()
237+
.ok()?;
238+
if !output.status.success() {
239+
return None;
240+
}
241+
let stdout = String::from_utf8_lossy(&output.stdout);
242+
let re = Regex::new(r"(?m)^\s*gateway:\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s*$").ok()?;
243+
return re
244+
.captures(&stdout)
245+
.and_then(|c| c.get(1).map(|m| m.as_str().to_string()));
246+
}
247+
248+
#[cfg(target_os = "linux")]
249+
{
250+
let output = Command::new("ip")
251+
.args(["route", "show", "default"])
252+
.output()
253+
.ok()?;
254+
if !output.status.success() {
255+
return None;
256+
}
257+
let stdout = String::from_utf8_lossy(&output.stdout);
258+
let re = Regex::new(r"(?m)^default\s+via\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\b").ok()?;
259+
return re
260+
.captures(&stdout)
261+
.and_then(|c| c.get(1).map(|m| m.as_str().to_string()));
262+
}
263+
264+
#[cfg(target_os = "windows")]
265+
{
266+
let output = Command::new("route").args(["print", "-4"]).output().ok()?;
267+
if !output.status.success() {
268+
return None;
269+
}
270+
let stdout = String::from_utf8_lossy(&output.stdout);
271+
let re = Regex::new(
272+
r"(?m)^\s*0\.0\.0\.0\s+0\.0\.0\.0\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s+",
273+
)
274+
.ok()?;
275+
return re
276+
.captures(&stdout)
277+
.and_then(|c| c.get(1).map(|m| m.as_str().to_string()));
278+
}
279+
280+
#[allow(unreachable_code)]
281+
None
282+
}
283+
223284
// ── Tauri Commands ─────────────────────────────────────────────────
224285

225286
#[tauri::command]
@@ -236,6 +297,21 @@ pub async fn remote_connect_get_device_info() -> Result<DeviceInfo, String> {
236297
})
237298
}
238299

300+
#[tauri::command]
301+
pub async fn remote_connect_get_lan_ip() -> Result<String, String> {
302+
lan::get_local_ip().map_err(|e| format!("get local ip: {e}"))
303+
}
304+
305+
#[tauri::command]
306+
pub async fn remote_connect_get_lan_network_info() -> Result<LanNetworkInfo, String> {
307+
let local_ip = lan::get_local_ip().map_err(|e| format!("get local ip: {e}"))?;
308+
let gateway_ip = detect_default_gateway_ip();
309+
Ok(LanNetworkInfo {
310+
local_ip,
311+
gateway_ip,
312+
})
313+
}
314+
239315
#[tauri::command]
240316
pub async fn remote_connect_get_methods() -> Result<Vec<ConnectionMethodInfo>, String> {
241317
ensure_service().await?;

src/apps/desktop/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ pub async fn run() {
557557
i18n_set_config,
558558
// Remote Connect
559559
api::remote_connect_api::remote_connect_get_device_info,
560+
api::remote_connect_api::remote_connect_get_lan_ip,
561+
api::remote_connect_api::remote_connect_get_lan_network_info,
560562
api::remote_connect_api::remote_connect_get_methods,
561563
api::remote_connect_api::remote_connect_start,
562564
api::remote_connect_api::remote_connect_stop,

src/web-ui/src/app/components/NavPanel/NavPanel.scss

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,76 @@ $_section-header-height: 24px;
620620
}
621621
}
622622

623+
.bitfun-nav-panel__remote-disclaimer {
624+
display: flex;
625+
flex-direction: column;
626+
gap: 10px;
627+
color: var(--color-text-secondary);
628+
padding: 12px 0 14px;
629+
}
630+
631+
.bitfun-nav-panel__remote-disclaimer-text {
632+
margin: 0;
633+
font-size: 12px;
634+
line-height: 1.2;
635+
color: var(--color-text-muted);
636+
}
637+
638+
.bitfun-nav-panel__remote-disclaimer-list {
639+
margin: 0;
640+
padding-left: 20px;
641+
display: flex;
642+
flex-direction: column;
643+
gap: 0;
644+
645+
li {
646+
font-size: 12px;
647+
line-height: 1.2;
648+
color: var(--color-text-secondary);
649+
padding-right: 0;
650+
}
651+
}
652+
653+
.bitfun-nav-panel__remote-disclaimer-actions {
654+
display: flex;
655+
justify-content: center;
656+
gap: 12px;
657+
margin-top: 6px;
658+
padding-top: 10px;
659+
border-top: 1px solid var(--border-subtle);
660+
}
661+
662+
.bitfun-nav-panel__remote-disclaimer-btn {
663+
border-radius: 6px;
664+
border: 1px solid transparent;
665+
min-width: 112px;
666+
padding: 7px 16px;
667+
font-size: 12px;
668+
cursor: pointer;
669+
transition: all $motion-fast $easing-standard;
670+
}
671+
672+
.bitfun-nav-panel__remote-disclaimer-btn--secondary {
673+
background: var(--element-bg-subtle);
674+
color: var(--color-text-secondary);
675+
border-color: var(--border-subtle);
676+
677+
&:hover:not(:disabled) {
678+
color: var(--color-text-primary);
679+
border-color: var(--border-medium);
680+
}
681+
}
682+
683+
.bitfun-nav-panel__remote-disclaimer-btn--primary {
684+
background: var(--color-accent-600, #2563eb);
685+
color: #fff;
686+
border-color: color-mix(in srgb, var(--color-accent-700, #1d4ed8) 35%, transparent);
687+
688+
&:hover:not(:disabled) {
689+
background: var(--color-accent-700, #1d4ed8);
690+
}
691+
}
692+
623693
// ──────────────────────────────────────────────
624694
// Reduced motion
625695
// ──────────────────────────────────────────────

src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useCallback } from 'react';
22
import { Settings, Info, MoreVertical, PictureInPicture2, Wifi } from 'lucide-react';
3-
import { Tooltip } from '@/component-library';
3+
import { Tooltip, Modal } from '@/component-library';
44
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
55
import { useSceneManager } from '../../../hooks/useSceneManager';
66
import { useToolbarModeContext } from '@/flow_chat/components/toolbar-mode/ToolbarModeContext';
@@ -10,6 +10,16 @@ import NotificationButton from '../../TitleBar/NotificationButton';
1010
import { AboutDialog } from '../../AboutDialog';
1111
import { RemoteConnectDialog } from '../../RemoteConnectDialog';
1212

13+
const REMOTE_CONNECT_DISCLAIMER_KEY = 'bitfun:remote-connect:disclaimer-agreed:v1';
14+
15+
const getRemoteDisclaimerAgreed = (): boolean => {
16+
try {
17+
return localStorage.getItem(REMOTE_CONNECT_DISCLAIMER_KEY) === 'true';
18+
} catch {
19+
return false;
20+
}
21+
};
22+
1323
const PersistentFooterActions: React.FC = () => {
1424
const { t } = useI18n('common');
1525
const { openScene } = useSceneManager();
@@ -21,6 +31,8 @@ const PersistentFooterActions: React.FC = () => {
2131
const [menuClosing, setMenuClosing] = useState(false);
2232
const [showAbout, setShowAbout] = useState(false);
2333
const [showRemoteConnect, setShowRemoteConnect] = useState(false);
34+
const [showRemoteDisclaimer, setShowRemoteDisclaimer] = useState(false);
35+
const [hasAgreedRemoteDisclaimer, setHasAgreedRemoteDisclaimer] = useState<boolean>(() => getRemoteDisclaimerAgreed());
2436

2537
const closeMenu = useCallback(() => {
2638
setMenuClosing(true);
@@ -53,14 +65,33 @@ const PersistentFooterActions: React.FC = () => {
5365
enableToolbarMode();
5466
};
5567

56-
const handleRemoteConnect = () => {
68+
const handleRemoteConnect = useCallback(async () => {
5769
if (!hasWorkspace) {
5870
warning(t('header.remoteConnectRequiresWorkspace'));
5971
return;
6072
}
73+
6174
closeMenu();
75+
76+
if (hasAgreedRemoteDisclaimer || getRemoteDisclaimerAgreed()) {
77+
setHasAgreedRemoteDisclaimer(true);
78+
setShowRemoteConnect(true);
79+
return;
80+
}
81+
82+
setShowRemoteDisclaimer(true);
83+
}, [hasWorkspace, warning, t, closeMenu, hasAgreedRemoteDisclaimer]);
84+
85+
const handleAgreeDisclaimer = useCallback(() => {
86+
try {
87+
localStorage.setItem(REMOTE_CONNECT_DISCLAIMER_KEY, 'true');
88+
} catch {
89+
// Ignore storage failures and keep in-memory consent for current session.
90+
}
91+
setHasAgreedRemoteDisclaimer(true);
92+
setShowRemoteDisclaimer(false);
6293
setShowRemoteConnect(true);
63-
};
94+
}, []);
6495

6596
return (
6697
<div className="bitfun-nav-panel__footer">
@@ -140,6 +171,53 @@ const PersistentFooterActions: React.FC = () => {
140171
<NotificationButton className="bitfun-nav-panel__footer-btn" />
141172
<AboutDialog isOpen={showAbout} onClose={() => setShowAbout(false)} />
142173
<RemoteConnectDialog isOpen={showRemoteConnect} onClose={() => setShowRemoteConnect(false)} />
174+
<Modal
175+
isOpen={showRemoteDisclaimer}
176+
onClose={() => setShowRemoteDisclaimer(false)}
177+
title={t('remoteConnect.disclaimerTitle')}
178+
showCloseButton
179+
size="large"
180+
contentInset
181+
>
182+
<div className="bitfun-nav-panel__remote-disclaimer">
183+
<p className="bitfun-nav-panel__remote-disclaimer-text">{t('remoteConnect.disclaimerIntro')}</p>
184+
<ol className="bitfun-nav-panel__remote-disclaimer-list">
185+
<li>{t('remoteConnect.disclaimerItemBeta')}</li>
186+
<li>{t('remoteConnect.disclaimerItemSecurity')}</li>
187+
<li>{t('remoteConnect.disclaimerItemEncryption')}</li>
188+
<li>{t('remoteConnect.disclaimerItemOpenSource')}</li>
189+
<li>{t('remoteConnect.disclaimerItemPrivacy')}</li>
190+
<li>{t('remoteConnect.disclaimerItemDataUsage')}</li>
191+
<li>{t('remoteConnect.disclaimerItemCredentials')}</li>
192+
<li>{t('remoteConnect.disclaimerItemQrCode')}</li>
193+
<li>{t('remoteConnect.disclaimerItemNgrok')}</li>
194+
<li>{t('remoteConnect.disclaimerItemSelfHosted')}</li>
195+
<li>{t('remoteConnect.disclaimerItemNetwork')}</li>
196+
<li>{t('remoteConnect.disclaimerItemBot')}</li>
197+
<li>{t('remoteConnect.disclaimerItemBotPersistence')}</li>
198+
<li>{t('remoteConnect.disclaimerItemMobileBrowser')}</li>
199+
<li>{t('remoteConnect.disclaimerItemCompliance')}</li>
200+
<li>{t('remoteConnect.disclaimerItemLiability')}</li>
201+
</ol>
202+
203+
<div className="bitfun-nav-panel__remote-disclaimer-actions">
204+
<button
205+
type="button"
206+
className="bitfun-nav-panel__remote-disclaimer-btn bitfun-nav-panel__remote-disclaimer-btn--secondary"
207+
onClick={() => setShowRemoteDisclaimer(false)}
208+
>
209+
{t('remoteConnect.disclaimerDecline')}
210+
</button>
211+
<button
212+
type="button"
213+
className="bitfun-nav-panel__remote-disclaimer-btn bitfun-nav-panel__remote-disclaimer-btn--primary"
214+
onClick={handleAgreeDisclaimer}
215+
>
216+
{t('remoteConnect.disclaimerAgree')}
217+
</button>
218+
</div>
219+
</div>
220+
</Modal>
143221
</div>
144222
);
145223
};

src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@
206206
max-width: 420px;
207207
}
208208

209+
.bitfun-remote-connect__description-link {
210+
color: var(--color-text-muted);
211+
text-decoration: underline;
212+
cursor: pointer;
213+
214+
&:hover {
215+
color: var(--color-text-muted);
216+
}
217+
}
218+
209219
.bitfun-remote-connect__error {
210220
font-size: 12px;
211221
color: var(--color-danger);
@@ -276,11 +286,17 @@
276286
transition: all 0.15s ease;
277287

278288
&--connect {
279-
background: var(--color-accent);
289+
background: var(--color-accent-600, #2563eb);
280290
color: #fff;
291+
border: 1px solid color-mix(in srgb, var(--color-accent-700, #1d4ed8) 35%, transparent);
281292

282293
&:hover:not(:disabled) {
283-
opacity: 0.9;
294+
background: var(--color-accent-700, #1d4ed8);
295+
}
296+
297+
&:focus-visible {
298+
outline: 2px solid color-mix(in srgb, var(--color-accent-500, #3b82f6) 55%, transparent);
299+
outline-offset: 1px;
284300
}
285301

286302
&:disabled {
@@ -326,11 +342,12 @@
326342
}
327343

328344
.bitfun-remote-connect__step-link {
329-
color: var(--color-accent);
345+
color: var(--color-text-muted);
330346
text-decoration: underline;
331347
cursor: pointer;
332348

333349
&:hover {
334-
opacity: 0.85;
350+
color: var(--color-text-muted);
335351
}
336352
}
353+

0 commit comments

Comments
 (0)