# WebPOS Passcode 登录

## 结构与边界

- `PasscodeLogin.tsx`：复用 materials 的 `PisellFind`、`Button`、现有 `TakeoverModal` 和 antd 的 Form/Alert/Result。
- `usePasscodeLogin.ts`：管理阶段、同步提交锁、流程编号、取消、失败重试和一次性完成回调。凭证只存在本轮组件内存中，提交成功或退出时释放。
- `service.ts`：通过现有宿主 request 验证 Passcode，校验企业状态，并用登录响应直接组装当前店铺及加载租户详情。
- `persistence.ts`：唯一的资料保存入口，要求已取得有效 link 结果及设备配置。等待必要插件操作，最后保存主 token，再同步外层 React 状态。
- `utils.ts` / `types.ts` / `locales.ts`：校验、展示转换、类型和五种语言文案。

Passcode 登录接口传 `noToast: true`，由表单错误区域展示接口 message；兼容宿主在该模式下 resolve 业务错误的行为，并在模块内重新抛出。租户详情、绑定和配置请求仍沿用宿主默认错误处理。Passcode 模块不保存完整请求错误，并继续校验登录资料、企业状态、租户归属和绑定结果。

Passcode 仍属于 `login` 步骤，不扩展通用 AuthMethodType。Login2 仅增加 `renderExtraLoginOptions` 插槽；LoginPanel 保持邮箱表单挂载并隐藏，不重置 Remember me。默认邮箱配置使用稳定引用，避免切换时重新回填记忆数据。

输入使用 `PisellFind` 现有的 `triggerType="input"`、`displayType="detached"`，关闭历史且不传 `resultRender`，无需修改公共组件或新增 `showResult`。保留内置输入框和 `showScan` 摄像头按钮：宿主提供 `webScan` 时调用该插件，否则打开 `PisellQRScanner`；`enableScanner` 另行接收扫码枪事件。两种扫码结果通过现有 `onChange` / `onSearch` 进入同一验证流程，忙碌或不可输入时隐藏摄像头入口并停止扫码枪监听。不使用会替换内置扫码按钮的 `inputRender`。

未复用会提前写入资料的邮箱登录、选择店铺和整套设备绑定处理器。未修改宿主项目、`@pisell/core`、邮箱保存协议或普通设备选择流程；未引入独立 HTTP 客户端或恢复标记。

## 流程

```text
输入 → 临时认证及企业状态校验 → 使用 tenant.default_domain 加载租户详情
  → number 非空时确认接管 → link → 设备配置 → 保存 → 成功页
  → 5 秒或点击 Continue → 现有 onIotDeviceLinked
```

倒计时初始值和截止时间共用 `REMAINING_TIME`（当前为 5 秒），每秒根据截止时间计算剩余秒数。满 5 秒时直接继续，不把显示值更新为 0；正常显示顺序为 5 → 4 → 3 → 2 → 1，不提前一秒结束。

租户详情请求失败只重试该阶段。绑定失败不会自动重发，主动重试需要再次确认；超时不表示服务端未完成绑定。配置和保存失败只重试对应阶段。保存不是跨 localStorage/NativeStorage/插件的原子事务，部分保存失败会明确显示错误，不重复绑定。

非验证阶段失败时，主按钮显示“重试”并从失败阶段继续，不重复绑定已成功的设备。输入框同时恢复编辑；如果用户修改通行码，则丢弃当前临时会话并重新开始。
## 宿主联调契约（尚待验证）

所有临时请求均传 `temporaryAuth: true, cache: false`。验证接口额外传 `isPisell2: true, noToast: true`；后续请求原样传完整 Bearer token，租户详情、link 和 setting 传本次 `tenantDomain`。请求头由主项目请求层统一设置，不单独构建 Accept、Content-Type、Locale 和 Timezone。

已核对当前主项目请求链未消费 `anonymous`，因此不再传入该字段。此前传 `anonymous: true` 也没有实现匿名鉴权，移除它不会清除或改变主项目默认附带的认证头。

宿主必须确认：

1. 如果登录接口要求匿名请求，宿主必须支持移除旧认证及租户头，后续拦截器不能补回；当前请求层尚未提供该保证。
2. 临时请求即使没有显式 token，也不能保存响应 Authorization。
3. 临时认证失败仅交给本流程，不能清除旧会话或全局跳转。
4. `tenantDomain` 只影响当前请求，普通并发请求不受影响。
5. Passcode、token、含凭证的请求/响应/错误对象不进入日志、埋点或缓存。
6. 绑定成功前，所有持久化存储及全局登录状态保持原状。

这些行为无法由组件 mock 测试证明。未完成宿主联调前，不应宣称真实设备端到端验收通过，也不能通过提前保存 token 绕过隔离要求。

保存阶段需要宿主提供 App storage 和 token 插件。NativeStorage 只在 App 宿主存在；普通 WebPOS 浏览器缺少该插件时，使用 App storage/localStorage 正常完成登录。`device.getDeviceDetail` 仍按既有约定可选。

## 验证

使用 Node 22；测试依赖版本复用仓库原有锁文件条目，没有迁移锁文件格式。

```sh
pnpm --filter @pisell/private-materials test:run src/plus/webPosLogin src/plus/saasDevice/devicePasscodes/__tests__
pnpm --filter @pisell/private-materials run build
git diff --check
```

Passcode Hook/UI 定向测试共 76 项通过，覆盖邮箱与 Passcode 主题色共享、Form.Item 错误态、`noToast` 下宿主 resolve 业务错误、原始 message 展示及完整登录流程。差异空白检查通过，包级 ES/CJS 构建及声明生成成功。没有执行根目录发布脚本、发布包或推送代码。

已核对主项目 `terra/src/utils/request/index.ts`：`customToast` 会被 private-materials 的 request 包装层覆盖，因此登录接口使用 `noToast`；同时对宿主 resolve 的业务错误做模块内抛出。主项目 HTTP 错误拦截器中的 `errors[]` Toast 当前不读取 `noToast`，如需连该路径也静默，宿主还需补充对应判断。

测试使用模拟 request、插件、存储和回调，没有使用真实通行码或真实设备。覆盖规范化/去重、业务失败、租户并行与校正、零写入、接管、配置及保存重试、异步等待、卸载和倒计时。摄像头入口测试使用真实 PisellFind/Lookup，模拟扫码器和宿主 `webScan`，覆盖打开/取消、结果回填、只提交一次、关闭历史及忙碌时隐藏入口；不代表真实摄像头采集已验证。

浏览器使用真实组件和样式、模拟请求及存储，在 1440×900、768×1024、375×667 和 320×568 下检查了输入、错误、接管及成功页。摄像头采集和真实硬件扫码仍需设备联调；扫码事件分发已通过真实 PisellFind 的组件测试。

已知仓库检查限制：

- 此前记录：原有 PisellFind 全文件测试存在 6 项失败，隔离运行当时修改前的组件及测试也复现了同样 6 项失败。本次已移除公共组件的 `showResult` 改动及对应测试，Passcode 使用真实 PisellFind 的 10 项 UI 测试通过。
- 包级 `tsc --noEmit` 被既有 `useReservationPhoneGridInfiniteData.test.ts` 中的 JSX 语法阻挡；缩小检查范围后，现有 React 类型声明和其他组件还存在类型错误。未为通过检查而修改这些文件或安装后的依赖源码。
