跳转到内容

手机扫码应用(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.1 双端点同屏紧凑卡片与单主“扫码”按钮

Section titled “1.1 双端点同屏紧凑卡片与单主“扫码”按钮”

打开应用后,主屏同屏展示端点 A(Endpoint A)与端点 B(Endpoint B)两张等权紧凑卡片(窄屏按 A → B 纵向排列,宽屏左右并排),底部操作栏固定唯一主按钮“扫码 / Scan”(action.scan):

connector-web 空闲状态下的双端点同屏卡片与底部单主扫码按钮
图为 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 与 profile generic.pair
图为 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)后,手机界面只提供**“退出观察”**操作。点击“退出观察”或直接关闭手机页面仅停止手机侧的状态轮询,绝不会取消任一端点上正在进行的配对。
  • 失败仅允许显式重试:若介绍或观察超时/中断,应用绝不在后台自动循环重试,仅在底部操作栏提供由用户显式触发的**“重试”**按钮。

点击页头右上角的齿轮图标可打开设置弹窗。当已有配对进行中时,对网络模式或 relay 地址的修改会作为草稿保存,并提示**“网络设置将在下一组配对生效”(关闭设置后点击页头右上角刷新图标**开始新一组配对,即可应用到新创建的连接器实例)。

connector-web 设置弹窗展示语言、主题与高级网络配置
图为 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 构建与包名迁移说明”

在仓库根目录加载 devenv 环境后:

终端窗口
cd apps/connector-android
printf '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.INTERNET
  • android.permission.ACCESS_NETWORK_STATE
  • android.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。