Skip to content

fix(shell): enforce the navigation policy when a top-frame load starts - #179

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/nav-policy-load-check
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/nav-policy-load-check

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:导航策略在顶层页面开始载入时也执行,所以每一次顶层载入都遵循同一策略。策略不在 WebView 里载入的页面会被停止,按策略去它该去的地方(网页进应用内浏览器,被拦截的哪儿也不去),WebView 回到服务器上 Console 原来所在的路由。
  • 为什么改:让「每次导航都经过导航策略」这条规则对每一次顶层载入都成立,按维护者已给出的裁决实施。
  • 风险与回滚:风险在误停合法页面;服务器同源页面(含 SPA 路由变化)、受信主机、登录外出页都保留,并有测试钉住。服务器页面在 Console 就绪前再次去往策略外页面时,显示新的失败页,不再反复重建。回滚即 revert 本 PR:纯 JS,无原生模块、无新权限,OTA 即可。
  • 先看:components/shell/ConsoleWebView.tsx 的 observeTopFrame 与「被替换 WebView 的报告丢弃」守卫;lib/shell/navigation-policy.ts 的 decideTopFrameLoad;components/shell/OfflineFallback.tsx 的新失败页。
  • 两分钟验证:npx jest --ci __tests__/components/ConsoleWebView.test.tsx __tests__/lib/shell/navigation-policy.test.ts,预期 2 个套件、108 个用例全部通过。
  • 需要你做的:在 Android 真机上验证一次:服务器内跳转(含 SPA 路由变化)、受信主机和登录外出页照常,策略外的顶层页面被停止并回到服务器。

What changes

  • The navigation policy is also enforced when a top-frame load starts, so every top-frame load follows it. decideTopFrameLoad (lib/shell/navigation-policy.ts) judges the URL the WebView reports with a load start or a load end, never the event alone (Android reports an in-app history change as a load start). A page on the server's origin stays, a route a native screen owns included, and so does anything decideNavigation loads in the top frame (a trusted host, about:, blob:).
  • The sign-in excursion judges its own pages first. A page the active excursion follows stays; its start, return, cancel and cap rules are unchanged. When the excursion ends at a reported URL by its own rule, its own re-create is the only one.
  • Anything else is stopped (stopLoading()), sent where the policy sends that URL, the same as for a request: the in-app browser for a web page, the OS for an app scheme, nowhere for a blocked page. The WebView is then re-created on the server, at the route the Console was on (the load state machine's recreate). Re-create rather than reload: a reload would load the stopped page again, while a new WebView drops the page and its back history, the path an excursion takes when it ends without returning.
  • A second stop before a Console is ready shows the failure page instead of re-creating again: the server's page itself went there again. New policy variant of OfflineFallback, "Can't show this page in the app", with Try again and Switch organization, in all ten locales. A Console that becomes ready (or a switch) clears it.
  • Reports from a WebView that a re-create is replacing are dropped: load start, load end and navigation state. They are never judged against the new WebView or against a new server after a switch, and a load end that sent the WebView back is not counted as the Console's.
  • docs/HYBRID-SHELL.md: the policy is enforced when a navigation is requested and again when a top-frame load starts. Changeset: mobile patch.

Tests

  • __tests__/lib/shell/navigation-policy.test.ts → decideTopFrameLoad: keeps the server's own pages (a native-owned route and the default port included); keeps trusted hosts, about:, blob:; stops any other page with where to open it; stops a blocked page opening nothing; judges every load as the top frame's.
  • __tests__/components/ConsoleWebView.test.tsx → new block "the navigation policy at load start": a load the policy does not keep is stopped, opened where the policy sends it, and the WebView goes back to the server at the Console's route; the same at load end, without that load end lifting the cover; a blocked load opens nothing; the server's own pages, an in-app history change and a native-owned route stay; a trusted host stays; a repeat before ready shows the failure page and opens nothing more; a ready Console in between allows a new re-create; the replaced WebView's reports are dropped, after a stop and after a switch.
  • Same file, sign-in excursion block: the excursion's pages stay at load start and load end and the excursion returns by its own rule; a plain http page ends it by its own rule, with one re-create; after 取消, the provider page's last report opens nothing.
  • The WebView mock keeps each mounted WebView's latest props, so a test reports through the instance that was replaced, as its native side would.

Acceptance notes

  • docs/HYBRID-SHELL.md gains one sentence. The failure-page variant and the re-create choice are described in code comments and the changeset.
  • A late onError / onHttpError from a WebView that is being replaced can still put up the failure page. This PR does not change that, and every re-create path already has it.
  • components/shell/ConsoleWebView.tsx, lib/shell/navigation-policy.ts and the touched tests follow Prettier in the changed hunks. Lines that were already off-format on main in __tests__/components/ConsoleWebView.test.tsx and docs/HYBRID-SHELL.md are left as they were.
  • Device verification on Android is the maintainer's (see 需要你做的).

Verification

All readings on head 328a832, origin/main = 668ed7e (an ancestor of the head).

Gates (exit codes, captured before any pipe)
npx tsc --noEmit                                         exit 0
npx eslint . --ext .ts,.tsx                              exit 0
npx jest --ci --coverage                                 exit 0  Test Suites: 44 passed, 44 total · Tests: 838 passed, 838 total
npx jest --ci --no-cache --runInBand ConsoleWebView      exit 0  (cold) Tests: 87 passed, 87 total
coverage: navigation-policy.ts 100/100/100/100 · ConsoleWebView.tsx 98.98 stmts / 93.82 branches · OfflineFallback.tsx 100 stmts / 92.85 branches
Ablation 1: the load-start check removed (8 red)

if (decision.kind === "stay") return; replaced by return; in observeTopFrame (anchor 1 → 0, blob 2227a64e9d0a → eeec8e8fdce7), restored to the HEAD blob with an empty git diff HEAD.

Tests: 8 failed, 100 passed, 108 total
red: stops a top-frame load the policy does not keep in the WebView, opens it where the policy sends it, and goes back to the server where the Console was
red: judges a load reported only at its end the same way, and never counts that load end as the Console's
red: stops a load the policy blocks, opening nothing
red: shows the failure page when the server's page goes there again before a Console is ready
red: goes back to the server again once a Console has been ready in between
red: drops the reports of the WebView it replaces
red: never judges a report from the WebView a switch replaced against the new server
red: (sign-in excursion) keeps the excursion's pages at load start and load end, and returns by its own rule
green: the keep cases (server pages, history change, native-owned route, trusted host) and the excursion's own end
Ablation 2: the replaced-WebView guard removed (4 red)

The four if (documentRef.current !== instance) return; guards replaced by a no-op (anchor 4 → 0, blob 2227a64e9d0a → 15d1976a663a), restored to the HEAD blob with an empty git diff HEAD.

Tests: 4 failed, 104 passed, 108 total
red: drops the reports of the WebView it replaces
red: judges a load reported only at its end the same way, and never counts that load end as the Console's
red: never judges a report from the WebView a switch replaced against the new server
red: (sign-in excursion) opens nothing for the provider's page after 取消

Generated by Claude Code

The navigation policy is also enforced when a top-frame load starts, so
every top-frame load follows it. decideTopFrameLoad judges the URL the
WebView reports at a load start or a load end, never the event alone: a
page on the server's origin stays, as does anything the policy loads in
the WebView; a page of an active sign-in excursion is the excursion's to
judge. Any other page is stopped, sent where the policy sends it (the
in-app browser or the OS), and the WebView is re-created on the server
where the Console was.

A second stop before a Console is ready shows the failure page instead of
re-creating again. Reports from a WebView that is being re-created are
dropped.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
A top-frame load the policy does not keep in the WebView is stopped,
opened where the policy sends it (nothing for a blocked page), and the
WebView goes back to the server where the Console was; at load end too,
without counting that load end as the Console's. The server's own pages,
an in-app history change, a route a native screen owns, a trusted host
and a sign-in excursion's pages stay; the excursion ends by its own
rules. A second stop before a Console is ready shows the failure page.
Reports from a WebView being re-created are dropped.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
The WebView mock keeps each mounted WebView's latest props, so a test
reports through the instance that was replaced, as its native side would,
including the render it got for a new server before a switch replaced it.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
… start; changeset

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Seat verdict: ACCEPT, landed by the seat under the maintainer ruling on #160 and the maintainer's in-session ruling for this order. Head-sha: 328a83214c7313e9ba436c5cfc9b1fcb15b1d77b

维护者速读

  • 改了什么: 页面开始加载时,再按导航策略检查一次顶层页面。不该在 App 内打开的页面会被停下,并按策略交给浏览器;WebView 随后在服务器上重建。如果同一个服务器页面反复跳出,会显示新的「无法在 App 中显示此页面」失败页,不会陷入循环。
  • 为什么改: 让每一次顶层加载都遵守导航策略。
  • 风险与回滚: 本服务器页面、应用内跳转、受信任主机、外出登录页面都不受影响,有测试钉住。纯 JS 改动,回滚就是 revert 本 PR。
  • 先看: lib/shell/navigation-policy.ts 的 decideTopFrameLoad,以及 components/shell/ConsoleWebView.tsx 的 observeTopFrame。
  • 两分钟验证: npx jest --ci __tests__/lib/shell/navigation-policy.test.ts __tests__/components/ConsoleWebView.test.tsx
  • 需要你做的:
    • 在安卓真机上走一遍企业 IdP 登录,确认外出登录正常。
    • 请会这些语言的同事过一遍新增的两条失败页文案(十种语言)。

席位核对

  1. Shape: draft → main. There is no Fixes line (no-card order). The PR carries Clause-②: no, Metadata-protocol: none, and a rule-level patch changeset.
  2. Gates:
    • Check-runs on head 328a832: Code Quality success, Lint & Type Check success. mergeable_state is clean.
    • Local jest --ci --coverage: 44/44 suites, 838 tests.
  3. Ablations:
    • Removing the stop: 8 tests go red.
    • Removing the replaced-WebView guards: 4 go red.
  4. Open questions, settled in seat:
    • Q1: keep the new failure-page variant. Its translations need native-speaker review.
    • Q2: the excursion start rule stays request-only. That is unchanged behaviour; widening it would need a ruling.
  5. Landing: the path surface is NOT governed, +422 / −16. EAS guard on main reads build and publish steps as skipped. The seat marks the PR ready and enables auto-merge into the merge queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 03:00
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 07c0952 Nov 3, 2026
2 checks passed
os-justin pushed a commit that referenced this pull request Nov 3, 2026
Stack both intents with #179 (navigation policy at load start) and #180
(pickImage, share, openFile): BRIDGE_METHODS carries both sets, the app
lock also refuses pickImage, share and openFile while locked, every
locale key from both sides is in all ten bundles, and the iOS permission
texts carry camera, photos and Face ID. pnpm-lock.yaml regenerated by
pnpm from main's. READ_EXTERNAL_STORAGE is no longer blocked: the image
picker and the file system module declare it too.

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.

1 participant