Repository navigation
Commit b62463f
feat(console): App lock row on the profile page inside the mobile shell (#29798)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbRXboSqkoBMor9JkZLKx8CK)_
Fixes #29748
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么**:在手机 App(原生壳)里打开「个人资料」页,如果 App
支持应用锁设置,会多出一行「应用锁」,显示当前是关闭,还是开启以及离开多久后锁定;点这一行打开 App 自带的应用锁设置页。
- **为什么改**:用户习惯在个人设置里找应用锁,此前它只能从 App 的连接切换面板进入。按
objectstack-ai/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 #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.
<details><summary>Red on main, ablation, tests, type-check,
lint</summary>
- 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).
</details>
<details><summary>Derived gates</summary>
`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
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jSM2R4osc3dyaqPHY1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4dc41e6 commit b62463f
17 files changed
Lines changed: 521 additions & 4 deletions
File tree
- .changeset
- ui
- apps/console/src/pages/system
- __tests__
- packages
- i18n/src/locales
- mobile
- src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
| 40 | + | |
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | | - | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
45 | 63 | | |
46 | 64 | | |
47 | 65 | | |
| |||
299 | 317 | | |
300 | 318 | | |
301 | 319 | | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
302 | 323 | | |
303 | 324 | | |
304 | 325 | | |
| |||
310 | 331 | | |
311 | 332 | | |
312 | 333 | | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
313 | 435 | | |
314 | 436 | | |
315 | 437 | | |
| |||
0 commit comments