Skip to content

feat(console): App lock row on the profile page inside the mobile shell - #29798

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-y37yg8
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-y37yg8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread
Fixes #29748

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:在手机 App(原生壳)里打开「个人资料」页,如果 App 支持应用锁设置,会多出一行「应用锁」,显示当前是关闭,还是开启以及离开多久后锁定;点这一行打开 App 自带的应用锁设置页。
  • 为什么改:用户习惯在个人设置里找应用锁,此前它只能从 App 的连接切换面板进入。按 Biometric app lock: cover the Console with a native lock screen after N minutes in the background mobile#145 裁决(方案 A),锁的开关只在 App 原生页面里、经生物识别后才能改,Console 只读不写,所以这一行没有开关。
  • 风险与回滚:只在 App 声明 openAppLockSettings 能力时出现,普通浏览器和旧版 App 页面不变;回滚即还原本 PR,无数据和服务端改动。
  • 先看:ui/apps/console/src/pages/system/ProfilePage.tsx(AppLockCard,尤其是重新读取状态的 effect)、ui/packages/mobile/src/nativeHost.ts(两个可选桥接方法和包装函数)、ui/packages/i18n/src/locales/ru.ts(俄语与阿拉伯语的时长句式改成冒号形式以避开格变化)。
  • 两分钟验证:pnpm --dir ui exec vitest run apps/console/src/pages/system/__tests__/ProfilePage.appLock-29748.test.tsx packages/mobile/src/__tests__/nativeHost.test.tsx,应全部通过(9 + 72)。
  • 需要你做的:无,席位落地。

What changes

The shell contract is objectstack-ai/mobile docs/HYBRID-SHELL.md, sections "App lock" and "Reading the app lock", read at objectstack-ai/mobile#181 head 77ded7bb. That PR is not merged to mobile main yet. The ruling is objectstack-ai/mobile#145 comment 6593856498, option A: the lock changes only on the shell's native screen, behind biometrics; the Console reads it and opens that screen, and never writes it.

  • Profile page (/system/profile and the Account app's account:profile_card, which mount the same page): a new AppLockCard renders only when nativeHostCan(useNativeHost(), 'openAppLockSettings'), the same feature-detect the header's organization button uses. It is one row: the title, and a state line from getAppLock(): "Off", "On, locks as soon as you leave the app" for a 0 timeout, or "On, locks DURATION after you leave the app". A tap calls nativeOpenAppLockSettings(). There is no toggle.
  • Re-read when shown. No shell event announces a change, so the card reads getAppLock() on mount, on visibilitychange back to visible, and on window focus. Only the newest read lands. A read the shell refuses leaves the row without a state line.
  • Duration. The timeout is named in its largest whole unit by Intl.NumberFormat (style: 'unit', unitDisplay: 'long') in the UI language, falling back to useDisplayLocale(), so no locale pack needs a count family and the machine locale is never used. ru and ar phrase it as "delay: DURATION", because Intl names the unit in the nominative case.
  • @object-ui/mobile: 'getAppLock' and 'openAppLockSettings' join NativeHostMethod; getAppLock?() and openAppLockSettings?() join ObjectStackNative as optional members (an older shell lacks them, as with scan?); new NativeAppLock type; new nativeGetAppLock() and nativeOpenAppLockSettings() wrappers with the same unavailable | ok | failed answer as the others. README documents them.
  • @object-ui/i18n: profile.appLock.{title,off,onImmediately,onAfter} in all ten packs.

In a plain browser, or a shell older than the app lock, nothing changes: no row, no bridge call.

Contract review

  • Widens: @object-ui/mobile gains two NativeHostMethod members, two optional ObjectStackNative members, the NativeAppLock type and two exported wrappers. @object-ui/i18n gains four keys under profile.appLock in every pack (the built en declaration types each leaf).
  • Narrows: nothing. No member, export or key is removed, renamed or made required. The new host members are optional and feature-detected, so a host object written against the old interface still type-checks, and an older shell is unaffected.
  • Writes: none. Neither wrapper sends arguments; the Console has no path that changes the lock, as the ruling requires.
  • No server, spec or metadata change.

Tests

  • New ProfilePage.appLock-29748.test.tsx (9 cases) renders the real ProfilePage and the real @object-ui/mobile reader against a fake window.ObjectStackNative:
    • On with a 5-minute timeout; on with 0 (immediately) and with an hour; off.
    • A tap calls openAppLockSettings once with no arguments, the card holds no switch or checkbox, and no other bridge method is called.
    • The state is read again on visibilitychange and on window focus after the lock changed.
    • A refused read leaves the row with no state line.
    • CONTROL: no bridge; a bridge without the method; a bridge whose has() does not announce it. Each draws no row and calls nothing.
  • nativeHost.test.tsx: both methods join the unavailable / has-says-no / method-missing rows; an ok case for the pair; a refused openAppLockSettings ("The app is locked") resolves failed.

Acceptance notes

  • The shell sends no change event. The row re-reads on visibilitychange and window focus. A native screen pushed inside the same Android activity may fire neither, so after returning from the settings screen the row can show the old state until the page is shown again. This is reported to the seat as a finding for the shell contract, not worked around here.
  • Serial with Console: use the mobile shell's pickImage(), openFile() and share() for photo capture, attachments and sharing #29735, which edits the same NativeHostMethod union, host interface, index.ts and test fake. Whichever lands second merges main and keeps both sets of members.

Verification

Head 75f435ad, based on 7ba8b11f. main has moved 18 commits since (other keys in the same ten locale packs); git merge-tree against it is clean, and CI runs on the merge ref.

Red on main, ablation, tests, type-check, lint
  • Red on main: with ProfilePage.tsx restored to 7ba8b11f (tree only; restored to HEAD after, git diff HEAD empty), the new file ran 6 failed, 3 passed: every shell case red, the three controls green.
  • Ablation (scripts/ablation-replace.mjs --delete of the visibilitychange and focus listener lines; restored, blob equals HEAD, git diff HEAD empty): 1 failed, 8 passed, the re-read case. Vitest aliases @object-ui/* to src, so no rebuild is involved.
  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/i18n/ packages/mobile/ plus the five ProfilePage.* suites: Test Files 109 passed, Tests 1633 passed, 13 skipped (the i18n suites include all-locales-key-parity, the untranslated-identity census and the machine-locale census).
  • turbo run type-check --filter=@object-ui/console --filter=@object-ui/mobile --filter=@object-ui/i18n: 73/73 tasks.
  • eslint on the five changed code files: 0 errors (one pre-existing set-state-in-effect warning on the page's name sync, untouched).
Derived gates

node scripts/pm/dispatch-gates.mjs --commands at 75f435ad derived 35 families. The 34 non-vitest families were run with exit codes captured before any pipe, with GITHUB_EVENT_PATH pointing at this body:

  • node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0
  • node scripts/check-adr-0087-registration.mjs --self-test: exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0
  • node scripts/check-closing-keyword-parity.mjs: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test: exit 0
  • node scripts/check-comment-mask-corpus.mjs: exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main: exit 0
  • node scripts/check-empty-changeset.mjs --self-test: exit 0
  • node scripts/check-sdui-manifest.mjs: exit 0
  • node scripts/check-sdui-manifest.mjs --self-test: exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test: exit 0
  • node scripts/release-pending-publish.mjs --self-test: exit 0
  • pnpm --filter @objectstack/spec run check:duration-unit-keys: exit 0
  • pnpm check:changeset-gate-self-tests: exit 0
  • pnpm check:closed-ceremony-text: exit 0
  • pnpm check:cross-package-test-inputs: exit 0
  • pnpm check:dispatcher-error-vocabulary: exit 0
  • pnpm check:driver-memory-census: exit 0
  • pnpm check:engine-double-contract: exit 0
  • pnpm check:gitlink-declared: exit 0
  • pnpm check:nul-bytes: exit 0
  • pnpm check:published-files: exit 0
  • pnpm check:query-options-erasure: exit 0
  • pnpm check:refd-timer-probe: exit 0
  • pnpm check:registry-log-declared: exit 0
  • pnpm check:rest-log-spy-declared: exit 0
  • pnpm check:skill-identifier-liveness: exit 0
  • pnpm check:test-source-alias: exit 0
  • pnpm check:tier-file-adoption: exit 0
  • pnpm check:type-check-coverage: exit 0
  • pnpm check:type-check-debt: exit 0
  • pnpm check:undeclared-dep-imports: exit 0
  • pnpm check:watch-hint-literal: exit 0
  • pnpm check:where-matcher: exit 0
  • pnpm --dir ui exec vitest run scripts/: Test Files 170 passed, 2 skipped; Tests 4977 passed, 2 skipped; exit 0
  • pnpm check:new-line-citations (from ui/): 0 new citations

Generated by Claude Code

The profile page shows the mobile shell's app-lock state from getAppLock()
and opens the shell's native settings screen through openAppLockSettings(),
only when the bridge announces it. The Console never writes the lock.
@object-ui/mobile gains the two optional bridge members and their wrappers;
the ten locale packs gain profile.appLock.*.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSM2R4osc3dyaqPHY1
…r the machine locale

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSM2R4osc3dyaqPHY1
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 75f435adf23a8c8827c75cf982ad1d9e348db4e9
Local-runs: none

① Derived judgments

  • @object-ui/mobile NativeHostMethod gains 'getAppLock' and 'openAppLockSettings'. The union is used only as the method parameter of nativeHostCan and callNative (grep of ui/ on main: no exhaustive switch or mapped record over it), so the wider union breaks no reader in the repo. Additive, correct.
  • ObjectStackNative gains getAppLock?() and openAppLockSettings?() as optional members, following the scan?/showConnectionSwitcher? pattern. A host object written against the old interface still satisfies it, and an older shell is feature-detected out through has(). Additive, correct.
  • New exports: the NativeAppLock type plus the nativeGetAppLock() and nativeOpenAppLockSettings() wrappers, re-exported from index.ts. Both go through callNative and return the same unavailable | ok | failed shape as the existing wrappers. Neither sends an argument, so the Console has no write path to the lock, as ruling Biometric app lock: cover the Console with a native lock screen after N minutes in the background mobile#145 option A requires.
  • @object-ui/i18n: four new leaves, profile.appLock.{title,off,onImmediately,onAfter}, in all ten packs. Additions only; onAfter interpolates one duration value in every pack.
  • Console ProfilePage AppLockCard is behaviour, not a contract face. It renders only behind nativeHostCan(..., 'openAppLockSettings').
  • Nothing narrows: no member, export, key or accepted value is removed, renamed or made required. No server, spec or metadata change.

② Semver level

The changeset declares patch for @object-ui/mobile, @object-ui/i18n and @object-ui/console, with Clause-②: yes (widening) and Metadata-protocol: none. AGENTS.md Post-Task step 3: a non-protocol change, widening or breaking, ships patch, and none takes patch. The level matches the declaration.

③ Boundary flags

Implemented-by: claude/project-thread-y37yg8
Reviewed-by: session_01QsR2jUJ19MstFvFhjgiYTu

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

席位核对 · ACCEPT · 2026-11-03T05:37Z

Head-sha: 75f435adf23a8c8827c75cf982ad1d9e348db4e9

  • Shape:
    • The PR is a draft against main. The first card line is Fixes #29748, under claim 6595311904.
    • 17 files, +521 / −4:
      • ProfilePage.tsx (+124 / −2, AppLockCard)
      • the new ProfilePage.appLock-29748.test.tsx
      • nativeHost.ts (+37 / −1), index.ts (+3), nativeHost.test.tsx (+31) and the mobile README
      • profile.appLock.* in all ten locale packs
      • a changeset
    • That is the claimed surface. check-governed-merges --pr 29798: not governed, 525 changed lines, under the human-merge threshold.
    • Clause-②: yes (widening) and Metadata-protocol: none start their lines in the body and the changeset, as claimed. All three packages ship patch.
  • Contract review: record 6596159679 on this head, declaration face, TIER_DEFAULT, PASS.
  • Read in the diff:
    • The card renders only behind nativeHostCan(useNativeHost(), 'openAppLockSettings'). A plain browser or an older shell draws no row and makes no bridge call.
    • The state line reads getAppLock() on mount, on visibilitychange to visible and on window focus, and only the newest read lands. A refused read leaves the title with no state line.
    • A tap calls nativeOpenAppLockSettings() with no argument. There is no toggle, so the Console has no write path, as ruling mobile#145 option A requires.
    • The duration uses Intl.NumberFormat unit style in the UI language, falling back to useDisplayLocale(), never the machine locale. ru and ar use the "delay: DURATION" form to avoid case agreement.
  • Pins changed: none. The new page test (9 cases, 3 CONTROLs) and the new nativeHost rows pin the change.
  • Gates (check-runs on this head): 44 success, 11 skipped, 0 failed or pending, Lint & Repo Gates and TypeScript Type Check both success. mergeable_state is clean.
  • Tests (dev):
    • Red on main: with ProfilePage.tsx at base, 6 of 9 are red and the three controls stay green.
    • Ablation: deleting the visibilitychange and focus listeners reds the re-read case.
    • 109 files and 1633 tests in i18n, mobile and the ProfilePage.* suites. Type-check 73/73, eslint 0 errors, and the 34 non-vitest derived gates exit 0.
  • NOT MEASURED: no live shell. The shell contract (mobile#181, 77ded7bb) is not merged on mobile main, so the row was measured only against a fake window.ObjectStackNative in jsdom.
  • Findings: filed as Shell: tell the Console when the app lock changes, so the profile page's App lock row does not stay stale after the native settings screen #29800 (class b, for the repo:mobile seat; the relay cannot write to objectstack-ai/mobile). The shell sends no app-lock change event, and an Android screen pushed inside the same activity fires neither visibilitychange nor focus. So after the native settings screen closes, the row can show the old state until the page is shown again.
  • Serial: seat 3's feat(console): take photos, open attachments and share records through the mobile shell #29803 (Fixes #29735) edits the same NativeHostMethod union, host interface and index.ts. This PR lands first, and feat(console): take photos, open attachments and share records through the mobile shell #29803 merges main and keeps both member sets.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #29748 closes the card on merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 05:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit b62463f Nov 3, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-y37yg8 branch November 3, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console: an "App lock" entry on the profile page when the mobile shell exposes openAppLockSettings()

1 participant