手机扫码应用(Web 与 Android)
iroh-pairing 提供两款共享同一 Rust 核心(connector-core)与共享设计资产(shared/design/)的手机端介绍应用:
connector-web(apps/connector-web/):基于 SolidJS + TypeScript + Vite 的纯静态 Web 应用,在浏览器中通过 WebAssembly 运行connector-core。connector-android(apps/connector-android/):基于 Kotlin + Jetpack Compose + CameraX 的原生 Android 应用,通过 UniFFI 绑定(crates/connector-android-bindings/)进程内调用原生connector-core。
两款应用遵循完全一致的状态机、文案契约与安全边界:只负责扫描两份邀请、检查基础兼容性、向两端递交介绍(Introduce)并观察完成进度(Observe),绝不代任一端点确认 SAS,也不读取或转发业务载荷。
1. 主界面布局与交互流程
Section titled “1. 主界面布局与交互流程”1.1 双端点同屏紧凑卡片与单主“扫码”按钮
Section titled “1.1 双端点同屏紧凑卡片与单主“扫码”按钮”打开应用后,主屏同屏展示端点 A(Endpoint A)与端点 B(Endpoint B)两张等权紧凑卡片(窄屏按 A → B 纵向排列,宽屏左右并排),底部操作栏固定唯一主按钮“扫码 / Scan”(action.scan):
图为 connector-web 浏览器端 360×800 界面(非 Android 真机截图):空闲状态下端点 A、B 卡片与底部唯一主“扫码”按钮同屏可见。
- 按序自动入槽:点击底部**“扫码 / Scan”**按钮后,应用自动针对下一个空槽位(先端点 A,再端点 B)打开扫码弹层。
- 常驻备用输入方式:扫码弹层除提供 4:3 相机取景框外,常驻**“改用粘贴邀请 / Paste invite instead”与“改用图片识别 / Choose image instead”**两个备用入口,无需开启相机权限也可完成配对。
- 卡内定向“重扫”:在介绍开始前,已填入邀请的卡片内部提供一个紧凑小字**“重扫 / Rescan”**按钮(保持 ≥ 48px / 48dp 最小触控尺寸),点击可定向替换该槽位的邀请。
1.2 自报服务摘要外露与未验证标注
Section titled “1.2 自报服务摘要外露与未验证标注”当槽位填入有效邀请后,卡片无需展开“详情”即默认外露显示:
- 端点自报的设备名称(
device_name); - 自报服务摘要:应用名称(
app_name)与原始等宽profile_id(例如pair-demo · generic.pair); - 明确的**“自报名称,尚未验证 / Self-reported, not verified”**提示,提醒用户卡片上的名称来自未验证的公开邀请声明,不能当作第三方机构颁发的认证徽章。
图为 connector-web 浏览器端界面:扫描端点 A 后,卡片直接展示自报服务名 pair-demo、generic.pair 及“自报名称,尚未验证”标注,并提供卡内“重扫”。
同时,当其中一个槽位已填入邀请、用户点击“扫码”为另一槽位读取第二份邀请时,扫码弹层会在取景框上方展示已扫描端点的自报服务摘要,方便用户在扫第二码前核对双方是否为同一业务服务。角色(role / accepts_roles)、用途(purpose)、权限范围(scope)与过期时间等字段收纳在卡片可展开的**“详情 / Details”**折叠区中。
1.3 第二份有效邀请自动开始介绍
Section titled “1.3 第二份有效邀请自动开始介绍”- 第一份有效邀请填入槽位 A 后,应用保持待命,等待录入端点 B。
- 当第二份有效且未过期的邀请录入并通过兼容性检查(相同
namespace、profile_id、profile_version且角色互补)后,应用自动触发一次Introduce,无需额外点击“开始连接”按钮。 - 一旦进入介绍阶段(
introducing),两个槽位立即锁定,卡内“重扫”入口隐藏。若第二份邀请无效、过期或与第一份不兼容,应用会保留已扫入的第一槽位并给出具体错误提示。
1.4 页头“开始下一组”、设置弹窗与退出观察
Section titled “1.4 页头“开始下一组”、设置弹窗与退出观察”- 页头刷新图标(开始下一组,
action.nextPair):位于页头右上角,用于清空当前配对组并创建全新的内存连接器实例(若当前配对正在进行中,会先弹出安全确认弹窗)。 - 页头齿轮图标(设置,
settings.title):打开独立的“设置”弹窗,管理界面语言(简体中文 / English)、明暗主题、网络模式/自定义 relay 草稿,并在弹窗底部的**关于(About)**区域展示项目仓库地址(https://git.gjz010.com/gjz010/iroh-pairing)、文档主页(https://iroh-pairing.gjz010.page)与开源许可证(Apache-2.0)。 - 退出观察(
Stop watching/detach):当两个端点均已确认收到介绍(handoff 完成,进入awaiting_verification)后,手机界面只提供**“退出观察”**操作。点击“退出观察”或直接关闭手机页面仅停止手机侧的状态轮询,绝不会取消任一端点上正在进行的配对。 - 失败仅允许显式重试:若介绍或观察超时/中断,应用绝不在后台自动循环重试,仅在底部操作栏提供由用户显式触发的**“重试”**按钮。
2. 网络模式与设置弹窗
Section titled “2. 网络模式与设置弹窗”点击页头右上角的齿轮图标可打开设置弹窗。当已有配对进行中时,对网络模式或 relay 地址的修改会作为草稿保存,并提示**“网络设置将在下一组配对生效”(关闭设置后点击页头右上角刷新图标**开始新一组配对,即可应用到新创建的连接器实例)。
图为 connector-web 浏览器端设置弹窗(图中自定义 relay 地址为本地自动化测试使用的 loopback 夹具地址)。
| 模式 | connector-android(原生) |
connector-web(浏览器 WASM) |
适用场景 |
|---|---|---|---|
| 自动(默认推荐) | native_internet:使用 iroh 官方 presets::N0(UDP 直连 + N0 relay + DNS 发现),启动即自动初始化 |
browser_relay:默认使用官方 N0 relay preset |
常规互联网或局域网环境,配合不带 --lan-only 的默认端点命令开箱即用 |
| 自定义 Relay | native_internet + 指定 HTTPS relay |
browser_relay + 指定 HTTPS relay |
使用自建或私有部署的 iroh relay 服务 |
仅局域网(--lan-only) |
native_lan:完全禁用 relay 与发现,仅使用邀请中的直连 UDP 地址,不等待 Endpoint::online() |
不可用(浏览器无法建立原始直接 UDP 连接) | 要求完全不访问任何外部 relay 或公网发现的纯离线局域网环境(两端 CLI 须同时传 --lan-only) |
为什么同一 Wi-Fi 下
connector-web仍需要 Relay? 浏览器环境中的 WebAssembly 无法直接收发原生 QUIC UDP 数据包,必须通过 WebSocket/HTTPS 连接到可达的 iroh relay 完成握手与 RPC 传输(通信内容仍保持端到端加密)。若使用connector-web扫描由--lan-only生成的不含 relay 提示的邀请,核心会在扫描阶段直接拒绝并提示relay_required。
3. connector-web 特性、PWA 与使用说明
Section titled “3. connector-web 特性、PWA 与使用说明”- 官方
/pair/发布路径与--connector-url规范:同一 Forgejo Pages 站点在根路径提供技术文档,并在子路径https://iroh-pairing.gjz010.page/pair/发布官方connector-web扫码器。注意 wire 协议与 CLI 参数规范保持不变:--connector-url(及Session::invite_url)仍必须使用无尾斜杠的https://iroh-pairing.gjz010.page/pair。 - PWA 主屏安装与静态离线缓存:
connector-web支持作为渐进式 Web 应用(PWA)安装到手机或桌面主屏幕。Service Worker 仅缓存静态界面与 WASM 资源(HTML、JS、CSS、.wasm、图标),因此离线状态下仍可打开界面壳;但浏览器端配对(browser_relay)仍需连接可达的 iroh relay(默认支持官方 N0 relay)。PWA 不缓存任何邀请、SAS 或配对状态,不执行后台配对或推送通知,也不会在活跃配对期间自动刷新页面。 - 首次 URL Fragment 同步捕获与擦除:当通过
https://<host>/pair#<envelope>(或托管目录跳转后的/pair/#<envelope>)打开页面时,index.html内联脚本会在任何 JS 模块、WASM 或网络请求加载前同步读取#<envelope>,立即调用history.replaceState清除地址栏 hash,并将邀请填入端点 A 槽位,防止一次性邀请泄露到请求 URL、浏览器历史或localStorage。 - 无持久秘密:
localStorage(键名connector-web.preferences.v1)仅保存主题与可选的自定义 relay URL,绝不保存邀请、token、事务 ID 或 SAS 数据。 - 相机安全上下文:浏览器调用相机(
getUserMedia)要求 HTTPS 安全上下文(仅localhost/127.0.0.1/[::1]开发环回允许 HTTP)。在非安全上下文或拒绝相机权限时,界面会明确说明原因并引导使用粘贴邀请或图片识别(内置本地 ZXing 解码与高密度二维码PURE_BARCODE采样回退)。 - 构建与部署详情:关于
build:pages(输出apps/connector-web/dist-pages并合并至apps/docs/dist/pair)及独立静态部署的 nginx / Caddy 配置,见部署指南。
4. connector-android 构建与包名迁移说明
Section titled “4. connector-android 构建与包名迁移说明”4.1 构建与安装命令
Section titled “4.1 构建与安装命令”在仓库根目录加载 devenv 环境后:
cd apps/connector-androidprintf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
# 构建默认 arm64-v8a debug APK 并运行 JVM 单元测试与 lint./gradlew --no-daemon testDebugUnitTest lintDebug assembleDebug
# 可选:构建包含 arm64-v8a + x86_64 双 ABI 的模拟器 debug APK./gradlew :app:assembleDebug -PincludeX86_64=true生成的默认可安装 APK 位于 apps/connector-android/app/build/outputs/apk/debug/app-debug.apk。应用最低支持 Android 10(API 29),仅声明三项权限:
android.permission.INTERNETandroid.permission.ACCESS_NETWORK_STATEandroid.permission.CAMERA
不申请外部存储、联系人、定位或后台常驻服务权限;从相册选择二维码图片通过系统 PhotoPicker / Content URI 完成。
4.2 包名(com.gjz010.irohpairing)与旧版卸载提示
Section titled “4.2 包名(com.gjz010.irohpairing)与旧版卸载提示”为明确项目由 gjz010 独立维护、避免与 iroh.computer / n0 官方域名混淆:
- Android 应用包名(
applicationId/namespace):已迁移为com.gjz010.irohpairing(Kotlin 绑定包名同步为com.gjz010.irohpairing.core)。 - 旧包不原地覆盖,可并存后按需卸载:Android 系统按
applicationId区分不同应用。若设备此前安装过旧包名computer.iroh.pairing.android的 APK,安装新包com.gjz010.irohpairing时不会覆盖旧应用,两者可在设备上并存;确认迁移完成后,可随时在系统设置中或通过以下命令卸载旧包:终端窗口 adb uninstall computer.iroh.pairing.android - 上游依赖名称保持不变:本次包名迁移仅针对本项目自身的 Android 应用与绑定命名空间;上游依赖 crate(如
iroh、iroh-gossip、vodozemac)及其原始许可证均原样保留。
更详细的实现与构建排查说明可参阅仓库内的 docs/web.md、docs/android.md 与 docs/android-bindings.md。