Skip to content

feat(connect): objectos://connect links and the QR scanner (screen 6) - #168

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-156-connect-qr-scanner
Nov 2, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-156-connect-qr-scanner

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #156

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:服务器选择页的地址栏右侧加了扫码按钮,打开全屏扫码页(关闭、手电筒、“将二维码放入框内”、“只包含服务器地址,不含任何账号信息”、“手动输入地址”);objectos://connect?server=… 链接(二维码或邀请邮件)会打开选择页,填好地址并自动探测。
  • 为什么改:管理员在 Console 里展示二维码,员工扫一下就能连上公司服务器,不用手抄地址;二维码只带服务器地址,不带任何账号或凭据。
  • 风险与回滚:新增原生依赖 expo-camera,必须重新出包(EAS build),OTA 更新带不上它;任何链接或扫码都要用户在确认页点“连接并登录”才会保存,不会静默换服务器。链接冷启动到选择页且已有保存的服务器时,地址页会出现“返回 该服务器”按钮(安卓返回键同效),回到原服务器的 Console。回滚:revert 本 PR 后重新出包。
  • 先看:lib/shell/incoming-link.ts(connectRequestFromLink 的拒收规则)、app/server-config.tsx(带 server 参数进入时自动探测的 effect,以及 showWayBack 返回原服务器的条件)、app.config.ts(相机插件与三语权限文案)。
  • 两分钟验证:npx jest --ci __tests__/lib/shell/incoming-link.test.ts __tests__/components/QrScanner.test.tsx __tests__/components/ServerConfigScreen.test.tsx,预期 3 个测试文件全部通过(29 + 15 + 21 个用例)。
  • 需要你做的:真机检查一轮(未验证,下文 Verification 末尾列了三项):扫真实二维码、从邮件或备忘录打开 objectos://connect 链接、拒绝相机权限。

What changes

  • objectos://connect links (lib/shell/incoming-link.ts). connectRequestFromLink reads the single server param and normalizes it with normalizeServerUrl. redirectIncomingPath sends a connect link to the picker as /server-config?server=…&n=…, never to the Console, so the user always sees the probe and the confirm step. A link with no server, a repeated server, any other param, a path after connect, or malformed encoding is refused and lands on the start screen, like every other unknown link. An address that does not normalize (plain http to a public host, not a URL, empty) still opens the picker, with the raw value in the field and its error shown.
  • Scanner screen 6 (components/shell/QrScanner.tsx, route app/connect-scan.tsx). The full-screen expo-camera scanner reads QR codes only on the back camera, and has close and torch controls, the hint, the note, and "Enter address manually". The component knows nothing about servers. The caller passes accept, the refusal text, and the optional note and manual action, so the bridge scan() (Bridge: scan() — barcode / QR scanning for Console fields #146) can reuse it as is. The route passes serverAddressFromCode, which accepts objectos://connect codes plus a plain https:// server URL; anything else shows "This isn't a connect QR code" and scanning continues. The first accepted code goes back to the picker with router.dismissTo, and the scanner stops reading. Camera permission is requested on open. If it is refused for good, the screen offers Settings and manual entry instead.
  • Picker (app/server-config.tsx: scan button, prefill, and the way back). The scan button is the address field's rightSlot. A server search param fills the field and runs runProbe. n changes on each request, so scanning the same code twice probes twice.
  • Way back to the saved server. When a server is saved and the picker has no screen behind it (router.canGoBack() is false, which is the case when a connect link cold-starts the app), the address step shows a "Back to" button naming the saved host (new key serverConfig.backTo in en, zh and ar). Android back on that step does the same thing. Both call router.dismissTo("/console") and save nothing. The confirm step is unchanged: "Not this server" still returns to the address step with the address kept, and the way back sits there. With no saved server, or when the picker was pushed over the Console, nothing is added. The screen-5 "where to find the address" copy is unchanged; the swap to Console's mobile-app page waits for Console: a "Mobile app" page under the user's settings that shows a connect QR code for the ObjectOS mobile app objectstack#29314.
  • Config (app.config.ts). The expo-camera plugin is configured for the camera only: microphonePermission: false and recordAudioAndroid: false. The iOS permission text is permissions.camera from each locale bundle (en, zh as zh-Hans, ar), written into the plugin and the per-language InfoPlist.strings through locales.
  • Dependency. expo-camera ~55.0.18, which is the version expo@55.0.26's bundledNativeModules.json pins for this SDK (pnpm resolved 55.0.23).
  • Copy. Added serverConfig.scan, serverConfig.backTo, scanner.* and permissions.camera to locales/{en,zh,ar}.json. Languages: follow the phone's language in the platform's 11 locales (ruling F of objectstack#29349) #159 (ten locales) had not landed when this was pushed (main at 29c25df), so there are three bundles.
  • Docs. docs/HYBRID-SHELL.md, Server selection now has a "Connect by QR code or link" section, plus updated Layout and Testing lists.

What the user sees when a server is already saved

Today there is one saved server; saved connections are #152.

  • The link opens while the app is running. The picker opens over the Console with the new address filled in and probing. The confirm step then shows the new server.
    • "Connect and sign in" replaces the saved server and opens the Console there, where the user signs in.
    • "Not this server" returns to the address step and the old server stays saved. The back gesture (iOS swipe, Android back) returns to the old server's Console.
  • The link cold-starts the app. The picker is the first screen. Confirming works the same way. After "Not this server", the address step shows "Back to old.example.com" (the saved host), and Android back does the same. Either one opens the saved server's Console with nothing saved.
  • Scanning from the picker. This is the same confirm step. Confirming replaces the saved server.

Tests

  • __tests__/lib/shell/incoming-link.test.ts: link parsing is covered for valid, encoded, unencoded, missing server, extra params, repeated server, http on a public host (insecure), http on a LAN host, malformed encoding, a non-connect host and a white-label scheme. Also covered: serverAddressFromCode accept and refuse, serverConfigHref, and the redirectIncomingPath connect cases.
  • __tests__/components/QrScanner.test.tsx mocks expo-camera. It covers permission loading, undetermined (asks once on open and again from the button), and blocked (Settings and manual entry, no request). It also covers refused codes (the message stays while the code is in view and clears REJECT_MESSAGE_MS after), the accepted code handed over once with reading stopped, torch on and off, close, and manual entry. Screen 6 itself is tested for its copy, refusing a code with an extra param, returning connect and https codes to the picker, and close or manual going back.
  • __tests__/components/ServerConfigScreen.test.tsx: the scan button opens /connect-scan. A linked address is probed and reaches confirm with nothing saved. With a server already saved, the user is still asked, and confirming replaces it. An unusable linked address shows in the field with its error. A new n probes the same address again. For the way back, there are four branches. (1) Saved server and no back stack: the button names the saved host; on the confirm step "Not this server" keeps the address and the button then appears; pressing it calls dismissTo("/console") and saves nothing. (2) Android back on that step does the same. (3) Saved server pushed over the Console: no button and no back handler. (4) No saved server: no button and no back handler.
  • The new component suite follows the established cold-load fix: a beforeAll warm-up render with a 30 s timeout, covering both permission branches.

Acceptance notes

  • Refused connect links go to the start screen. The card says to reject other params but does not name a destination. A refused link is handled like any other unknown link on the scheme. No partial value is prefilled, so nothing from a link that carries extra data reaches the field.
  • Scheme. For a scanned QR code, only the literal objectos scheme is accepted, as the card specifies. An incoming link is read on whatever scheme the OS delivered it on, as the existing open and console links are, so white-label builds (EXPO_PUBLIC_APP_SCHEME) work.
  • Android side effect. locales also writes values-b+en, values-b+zh+Hans and values-b+ar folders with an empty strings.xml, because the Android camera prompt is system text. Measured in an expo prebuild of this branch. Harmless.
  • Changeset level. The changeset is minor with Metadata-protocol: none, following the feat(server-picker): probe ObjectOS discovery and confirm the server before connecting #165 changeset already in this repo. This repo has no Check Changeset gate.

Verification

All local gates ran on head 0f04604, after merging origin/main at 29c25df. The commands are the same as ci.yml and lint-typecheck.yml.

Gates: tsc, eslint, jest --ci --coverage (all exit 0)
  • npx tsc --noEmit: exit 0
  • npx eslint . --ext .ts,.tsx: exit 0
  • npx jest --ci --coverage: exit 0. Test Suites 31 passed of 31, Tests 411 passed of 411, coverage thresholds met. incoming-link.ts has 100 % lines and 98.18 % branches; QrScanner.tsx has 100 % lines and 95.34 % branches.
  • pnpm install --frozen-lockfile: exit 0
Cold transform cache, as on a fresh CI runner

npx jest --ci --coverage --no-cache --maxWorkers=2 --verbose: exit 0, 411 of 411. The way-back tests took 5 to 23 ms each. The warm-up beforeAll absorbs the first-render load.

Ablation: connect links no longer reach the picker

The ablation deleted the connect branch of redirectIncomingPath on the committed head. The anchor occurrence count went from 1 to 0, and the marker count was 1. Result: incoming-link.test.ts exit 1 with 2 failed of 29: "opens the server picker for a connect link, never the Console" and "opens the picker with the raw value when the connect address is unusable". The file was restored with git checkout HEAD. The blob hash matched HEAD (dd704c1) and git diff HEAD was empty. The rerun exited 0 with 29 of 29.

Ablation: the way back removed

The ablation replaced showWayBack with false on the committed head 0f04604. The anchor occurrence count went from 1 to 0, and the marker count was 1. Result: ServerConfigScreen.test.tsx exit 1 with 2 failed of 21: "cold-started onto the picker with a server saved: offers it, and confirm's 'Not this server' keeps it" and "Android back on the address step does the same". The file was restored with git checkout HEAD. The blob hash matched HEAD (fcfd873), and git diff HEAD and git status were both empty. The rerun exited 0 with 21 of 21.

Native config: expo prebuild of this branch, in a throwaway copy
  • ios/ObjectOS/Info.plist has NSCameraUsageDescription with the English text and no NSMicrophoneUsageDescription.
  • en.lproj, zh-Hans.lproj and ar.lproj each have InfoPlist.strings with that language's NSCameraUsageDescription.
  • AndroidManifest.xml has android.permission.CAMERA and no RECORD_AUDIO.

Not verified: device checks for the maintainer.

  1. Scan a real connect QR code from the Console's mobile-app page, or a generated objectos://connect?server=… code. The confirm step should show your server.
  2. Open an objectos://connect link from an email or Notes, once with the app running and once with it closed. The picker should open, probe, and confirm. With a server already saved and the app closed, tap "Not this server". "Back to" the saved host (and Android back) should return to that server's Console.
  3. Deny camera permission. The scanner should offer Open Settings and Enter address manually. On a zh or ar phone, the system prompt should be in that language.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Seat verdict: ACCEPT — Head-sha: 0f0460473ed89937ab320569b5c64ad798ee41ea

维护者速读

  • 改了什么: 服务器选择页加了扫码按钮和全屏扫码页(只读二维码,有手电筒、手动输入)。objectos://connect?server=… 链接(二维码或邀请邮件)会打开选择页,填好地址并自动探测,仍然要你在确认页点「连接并登录」才保存,不会悄悄换服务器。扫码组件做成可复用的,Bridge: scan() — barcode / QR scanning for Console fields #146 的桥接 scan() 直接用。
  • 为什么改: 管理员在 Console 里展示二维码,员工扫一下就能连上公司服务器,不用手抄地址。二维码只含服务器地址,不含任何账号或凭据。
  • 风险与回滚: 新增原生依赖 expo-camera(只申请相机,不申请麦克风),必须重新出包,OTA 带不上。冷启动经链接进入且已存有服务器时,地址页有「返回 <原服务器>」入口(安卓返回键同效),这是席位复核时要求补上的。参数不对的连接链接一律回到启动页。回滚:revert 本 PR 后重新出包。
  • 先看: lib/shell/incoming-link.ts 的 connectRequestFromLink(拒收规则)→ app/server-config.tsx(带参数自动探测、showWayBack)→ app.config.ts(相机插件与三语权限文案)。
  • 两分钟验证: 运行 npx jest --ci __tests__/lib/shell/incoming-link.test.ts __tests__/components/QrScanner.test.tsx __tests__/components/ServerConfigScreen.test.tsx。
  • 需要你做的: ① 真机检查三项:扫真实二维码;从邮件或备忘录打开链接,App 运行中和已关闭各一次,关闭状态下点「不是这台服务器」后应能「返回」原服务器;拒绝相机权限。本 PR 未上真机。② 审批并合并,合并后要重新 EAS 出包。

席位意见: 「不会静默连接」这条有测试钉住。消融两腿:去掉链接分支,2 个用例转红;去掉返回入口,2 个用例转红。连接链接参数不对时回启动页(dev 选项 A),席位接受,因为不新增界面,也不把链接里多带的数据填进来。建议合并。

席位核对

  1. Shape: draft → main. First line Fixes #156; it is the only closing keyword. Clause-②: no and Metadata-protocol: none are present. The changeset is minor, following feat(server-picker): probe ObjectOS discovery and confirm the server before connecting #165's precedent. The seat rewrote the body after patch round 1, with a signed trailer.
  2. Premise, measured: main at 83d4716 had no expo-camera and a documented scan seam (app/server-config.tsx:47-49). expo-camera ~55.0.18 matches the SDK 55 bundledNativeModules pin.
  3. Gates: check-runs on head 0f04604: Code Quality success, Lint & Type Check success. mergeable_state is clean. Local: jest --ci --coverage 31/31 suites, 411 tests, plus a cold run with the same result.
  4. Accepted deviations:
    • Android back on the address step also takes the way back, under the same condition as the button.
    • The changeset is minor.
  5. Out of scope:
  6. Landing:
    • Path surface NOT governed. app.config.ts and package.json are not governed paths.
    • The maintainer merges, and a native rebuild is needed.

@objectstack-fleet objectstack-fleet Bot added the needs-user-decision Blocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review label Nov 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 13:05
@os-zhuang
os-zhuang merged commit 728b3c5 into main Nov 2, 2026
2 checks passed
@objectstack-fleet objectstack-fleet Bot removed the needs-user-decision Blocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review label Nov 2, 2026
os-justin pushed a commit that referenced this pull request Nov 2, 2026
Stack #168's connect-by-QR work with the ten-locale shell: the picker keeps
the scan button, link prefill and way-back from #168 and the RTL direction
from #169; #168's new keys (serverConfig.scan, serverConfig.backTo, scanner.*,
permissions.camera) are carried into ja ko de fr es pt ru; app.config.ts
localizes the iOS camera prompt in all ten languages.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Connect by QR code: objectos://connect?server=… links and the native scanner screen

2 participants