pipwave-ekyc-uikit 3.7.0-beta.1 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # eKYC Web UI review — sc-359303 (presigned S3 upload for ID, selfie and CC images) — simple version
2
+
3
+ **Counts:** 0 Critical · 1 Major · 2 Minor · 2 Enhancement · **5 total**
4
+
5
+ Project: repo `pw-ekyc-web-ui`, npm package `pipwave-ekyc-uikit`.
6
+ MR: https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/merge_requests/102 (target `release-beta`)
7
+ Reviewed at: `0c7493e21ae486715d12d7b3fe350baa958e2cad`
8
+
9
+ ## What this is about
10
+
11
+ Photos of IDs, selfies and credit cards used to be squeezed into the same request that submitted them. Now each photo is uploaded straight to storage first, and only a link is submitted. That is much lighter, but one request has turned into several, so there are more places where things can go wrong.
12
+
13
+ Round 2. Everything raised last time has been dealt with. The five items below are new, and most of them are small side effects of last round's fixes.
14
+
15
+ ## Major
16
+
17
+ ### 1. [package.json:16-18](package.json#L16-L18)
18
+
19
+ The package now tells anyone installing it that they must be on a fairly new version of Node, even though the shipped code never runs on Node at all. The hosted page project is set up to treat that as a hard error, so on an older machine the install stops instead of just warning. It does not bite today because the build servers are already new enough, but it is a trap waiting for someone on an older setup. Keep the Node requirement in this repo's own config files instead of shipping it to everyone.
20
+
21
+ ## Minor
22
+
23
+ ### 2. [src/pageClass/VerifyId.ts:528](src/pageClass/VerifyId.ts#L528), [src/pageClass/StartSelfie.ts:119](src/pageClass/StartSelfie.ts#L119), [src/pageClass/VerifyCc.ts:173](src/pageClass/VerifyCc.ts#L173)
24
+
25
+ When something unexpected fails, the code now writes a note to the browser console, but that note leaves out the bit that says what actually went wrong. Support will see that a session failed and still not know why, which was the whole point of last round's fix. Log the full error object instead of just its trace.
26
+
27
+ ### 3. [src/pageClass/StartSelfie.ts:122](src/pageClass/StartSelfie.ts#L122), [src/pageClass/VerifyCc.ts:175](src/pageClass/VerifyCc.ts#L175)
28
+
29
+ The failure signal sent to the merchant's own page used to include the error and now includes nothing at all. That is safer, since the old version leaked the image link, but the merchant can no longer tell why their customer failed. Nobody appears to be reading it today, so this is not urgent. Send a short reason with no request data attached.
30
+
31
+ ## Enhancement
32
+
33
+ ### 4. [src/helper/imagesHelper.ts:136](src/helper/imagesHelper.ts#L136)
34
+
35
+ A leftover from an earlier version: something is shared out of this file that nothing else uses. Harmless, just clutter. Delete it.
36
+
37
+ ### 5. [src/pageClass/StartSelfie.ts:73](src/pageClass/StartSelfie.ts#L73)
38
+
39
+ An empty line left behind where a removed pop-up used to be. Tidy it up.
40
+
41
+ ## Last round
42
+
43
+ All twelve earlier points check out against the current code, and the project type-checks, tests (90 passing) and builds cleanly with the real published SDK installed. The only one taken on trust is the storage cleanup point, which depends on a backend rule that cannot be seen from this repo.
@@ -0,0 +1,85 @@
1
+ # eKYC Web UI review — sc-359303 (presigned S3 upload for ID, selfie and CC images)
2
+
3
+ **Counts:** 0 Critical · 1 Major · 2 Minor · 2 Enhancement · **5 total**
4
+
5
+ Project: repo `pw-ekyc-web-ui`, npm package `pipwave-ekyc-uikit`.
6
+ Paired change: eKYC Web SDK (repo `pw-ekyc-web-sdk`, package `pipwave-ekyc-sdk`) !45 / sc-359302, the typed `uploadFile`. Not to be confused with each other: this MR is the UI kit, !45 is the SDK it depends on.
7
+ MR: https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/merge_requests/102 (target `release-beta`)
8
+ Reviewed at: `0c7493e21ae486715d12d7b3fe350baa958e2cad`
9
+
10
+ Round 2. All twelve findings from round 1 are addressed — details under "Round 1 status" below. The five findings here are new, and four of them are second-order consequences of those fixes.
11
+
12
+ ## What this change does and why
13
+
14
+ ID, selfie and credit card images stop travelling as base64 inside the submit request. Each frame now asks the eKYC Web SDK for a presigned S3 URL, uploads the JPEG directly, and the submit call carries only the returned read URL in the existing `*_uri` fields. That removes megabytes of base64 from the API request body and moves the image bytes onto a path built for them.
15
+
16
+ The cost of that design is that one atomic request becomes several: up to four uploads plus the submit, all in front of error handling that was written for a single call. Round 1 concentrated on that new failure surface, and the fixes for it — mirroring `VerifyPoa` by letting SDK errors propagate raw, replacing the forwarded error with a `warn()` log, and pinning the test runtime — are where the remaining findings sit. One touches what a consumer of `pipwave-ekyc-uikit` sees at install time; two touch how much information survives a failure; two are tidiness.
17
+
18
+ ## Major
19
+
20
+ ### 1. The published `engines` field can hard-fail a consumer's install
21
+
22
+ [package.json:16-18](package.json#L16-L18)
23
+
24
+ `"engines": { "node": ">=20.19" }` is a build-tooling requirement — vitest 4's config loader needs `require(esm)` — but `engines` is published with the package and evaluated on every consumer's install, not just in this repo. Nothing in the shipped browser bundle needs Node at all.
25
+
26
+ `pw-hosted-page` has `engine-strict=true` in its `.npmrc`, which turns a dependency engine mismatch from a warning into a hard error. Reproduced against a stub package with these exact `engines` under Node 18: `npm error code EBADENGINE ... Required: {"node":">=20.19"}`, install aborted. Latent rather than live today — that repo's CI runs `node:22-alpine` and its `.nvmrc` says `20`, which nvm resolves to 20.19+ — but `pw-demo` still declares `engines.node` as `^20 || ^18 || ^16 || ^14.19`, so anyone building it on 18 picks up the mismatch.
27
+
28
+ Suggest keeping the requirement local to this repo — `.nvmrc` plus a README note, which already covers the actual audience — or moving it to `devEngines` so it stays out of the consumer's dependency resolution.
29
+
30
+ ## Minor
31
+
32
+ ### 2. `warn()` logs only the stack, so the field that identifies the failure never reaches the console
33
+
34
+ [src/pageClass/VerifyId.ts:528](src/pageClass/VerifyId.ts#L528), [src/pageClass/StartSelfie.ts:119](src/pageClass/StartSelfie.ts#L119), [src/pageClass/VerifyCc.ts:173](src/pageClass/VerifyCc.ts#L173)
35
+
36
+ `warn` in `src/helper/console.ts` logs `console.warn(err.stack)` and nothing else. A `PwReqError` carries the diagnosis in `type`, `code`, `status` and `response.data` — none of which appear in a stack string, whose first line is only `PwReqError: <message>`.
37
+
38
+ These three call sites are the generic fallback branch, reached precisely when `err.type` did not match any of the routed lists. So the one thing support needs in order to say why a session failed is the one thing that is dropped, which leaves finding 4 of round 1 half-closed: the wrapper no longer hides the error, but the logger does.
39
+
40
+ Suggest `console.warn("[pw-ui-sdk] " + msg, err)` in `console.ts` so the whole object is inspectable, or logging `err.type` and `err.status` alongside the stack at the call sites.
41
+
42
+ ### 3. The merchant's `fail:ekyc` event now carries `undefined` for selfie and CC failures
43
+
44
+ [src/pageClass/StartSelfie.ts:122](src/pageClass/StartSelfie.ts#L122), [src/pageClass/VerifyCc.ts:175](src/pageClass/VerifyCc.ts#L175)
45
+
46
+ Dropping the argument to `openErrorPage()` does stop the presigned link leaking, which is what finding 3 of round 1 asked for. It also means `onFailedEkyc(undefined)` dispatches `fail:ekyc` with `detail: undefined`, where these two pages previously handed over the error. A merchant handler reading `event.detail.type` gets nothing, and one reading it unguarded throws.
47
+
48
+ Nothing in `pw-hosted-page`, `pw-demo` or the wiki reads `detail`, and `VerifyId` already behaved this way before this MR, so this is latent and it does make the three pages consistent. The gap is that the event went from over-sharing to sharing nothing.
49
+
50
+ Suggest forwarding a trimmed object — `{ type: err?.type, message: err?.message }` — so the merchant keeps a reason without the request body riding along, or confirming that an empty `detail` is the intended contract and noting it in the wiki.
51
+
52
+ ## Enhancement
53
+
54
+ ### 4. `UploadFileType` is re-exported but nothing imports it
55
+
56
+ [src/helper/imagesHelper.ts:136](src/helper/imagesHelper.ts#L136)
57
+
58
+ `export type { UploadFileType };` has no consumer — `src/index.ts` exports only `PWUISDK`, and no file in `src/` imports the symbol from here. `DocUploadType` on the next line is used, but only inside this module, so its `export` is doing nothing either. Both were useful while the type was defined locally; now that it comes from `pipwave-ekyc-sdk`, a caller that needed it would import it from there.
59
+
60
+ Suggest dropping the re-export and the `export` keyword on `DocUploadType`.
61
+
62
+ ### 5. Stray blank line left by the removed retake dialog
63
+
64
+ [src/pageClass/StartSelfie.ts:73](src/pageClass/StartSelfie.ts#L73)
65
+
66
+ A blank line sits directly after `} catch (err: any) {`, where the upload-error branch used to be. It reads as diff noise in a file whose other `catch` blocks start immediately with their first comment.
67
+
68
+ Suggest removing the line.
69
+
70
+ ## Round 1 status
71
+
72
+ All twelve threads are still open on the MR, each with a reply. Verified against `0c7493e2`:
73
+
74
+ - **1, 9, 10** — `ImageUploadError`, the retry loop and the three retake dialogs are gone from the tree; `grep` for `ImageUploadError`, `isImageUploadError` and `error_image_upload_failed` returns nothing. SDK errors now reach the existing routing raw.
75
+ - **2** — the pin is `pipwave-ekyc-sdk: 2.3.0-beta.1` (exact), the cast is gone, and `uploadFile` is called against the real three-argument signature. `tsc --noEmit`, `npm test` (90 passing) and `npm run build` all pass against the installed package.
76
+ - **3** — both generic fallbacks call `openErrorPage()` with no argument. See finding 3 for the second-order effect.
77
+ - **4** — `dataUrlToFile` attaches the original as `cause`, and the wrapper that buried the SDK error is gone. See finding 2 for what the replacement logger drops.
78
+ - **5** — no abort added, declined with a backend rationale and a comment on `uploadFrames`. The lifecycle rule is in `serverless-ekyc` and not verifiable from here; taking it as stated.
79
+ - **6** — `IdDocUris` / `CcDocUris` / `SelfieUris` are `Pick<>` from the SDK's own param types and `UploadFileType` is imported from the package root. Confirmed a rename upstream would now fail the build.
80
+ - **7** — `engines` and `.nvmrc` added. See finding 1.
81
+ - **8** — the suite now asserts a `PwReqError` propagates unchanged through `uploadImageDataUrl` and `uploadDocSides`, plus the empty-input cases. The tests that protected the wrapper are gone with it.
82
+ - **11** — decoding is `Uint8Array.from(atob(...))`, the fallback offered in that thread, chosen over `fetch(data:)` for merchant-CSP reasons. Sound trade-off; the per-byte cost noted in round 1 stands but was accepted then.
83
+ - **12** — one generic `uploadFrames<K>`; both callers only build their jobs array.
84
+
85
+ Also checked and found correct: `uploadId` / `uploadCc` / `uploadSelfie` required-field validation still resolves the same way with `*_uri` fields as it did with base64, so a missing frame fails where it used to; `uploadDocSides` maps the CC back side to `back_doc_*` and the SDK's `uploadCc` picks `back_doc_uri` as its required field; the `imagesHelper` → `uisdk` import matches four existing helpers and components; `Uint8Array.from` with a map function is within the `es2016` compile target.
@@ -0,0 +1,15 @@
1
+ # Slack drafts: pipwave-ekyc-uikit 3.7.0
2
+
3
+ ## #hello-serverless (eKYC Web UI) — SENT
4
+
5
+ pipwave eKYC Web UI Release 3.7 is ready:
6
+ • MR
7
+ https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/merge_requests/106
8
+ • Release Notes
9
+ https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/wikis/**Release-3.7-Deployment-Notes#release-37
10
+
11
+ No cc this round (Jun Jie is not in the channel).
12
+
13
+ Sent: https://hydron-holdings.slack.com/archives/C017T2JCYNT/p1789441645064619
14
+
15
+ Thread reply when the deploy starts (manual, posted by you): deploying
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipwave-ekyc-uikit",
3
- "version": "3.7.0-beta.1",
3
+ "version": "3.7.0",
4
4
  "description": "",
5
5
  "main": "dist/pw-bundle.js",
6
6
  "module": "dist/pw-bundle.mjs",
@@ -1,16 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(GITLAB_HOST=gitlab.g2g.com glab mr list --repo pipwave-frontend/pw-ekyc-web-ui)",
5
- "Bash(GITLAB_HOST=gitlab.g2g.com glab mr view 94 --repo pipwave-frontend/pw-ekyc-web-ui)",
6
- "Bash(GITLAB_HOST=gitlab.g2g.com glab api \"projects/pipwave-frontend%2Fpw-ekyc-web-ui/merge_requests/94/versions\")",
7
- "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d[0],indent=2\\)\\); print\\('total versions:',len\\(d\\)\\)\")",
8
- "Bash(echo \"=== tsc exit: $? ===\")",
9
- "Bash(npx vitest *)",
10
- "Bash(node -p \"const p=require\\('./package.json'\\);JSON.stringify\\({v:p.devDependencies.vitest,c:p.devDependencies['@vitest/coverage-v8']}\\)\")",
11
- "Bash(GITLAB_HOST=gitlab.g2g.com glab api \"projects/pipwave-frontend%2Fpw-ekyc-web-ui/merge_requests/94/discussions?per_page=100\")",
12
- "Bash(python3 -c ' *)",
13
- "Bash(git diff *)"
14
- ]
15
- }
16
- }
@@ -1,47 +0,0 @@
1
- # eKYC Web UI review — sc-356160 (fix face model-load deadlock, add stall-recovery UX) — simple version
2
-
3
- **Counts:** 0 Critical · 0 Major · 3 Minor · 1 Enhancement · **4 total**
4
-
5
- Project: repo `pw-ekyc-web-ui`, npm package `pipwave-ekyc-uikit`.
6
- Not to be confused with the **eKYC Web SDK** (repo `pw-ekyc-web-sdk`, package `pipwave-ekyc-sdk`).
7
-
8
- Branch: `hotfix-teojunjie-sc-356160-fix-face-model-load-deadlock`
9
- MR: https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/merge_requests/94 (target `release-beta`)
10
- Reviewed at: `0f1a50ccd1b1aec5b54c0d414f20372396cc3a12`
11
- Compared against: `origin/release-beta`
12
-
13
- **Round 2.** All 13 comments from the first round are resolved, and the newest commit genuinely fixes them, with tests: both bigger problems (a browser-cancelled download leaving the screen stuck, and the half-installed cancel patch that never retried) are properly closed, the confusing flag names are renamed, and the unused jsdom package is gone. Of the old points only two survive, and two new small ones come out of the fix itself.
14
-
15
- **Scope note:** points about the new automated-test setup itself were left out on purpose. That setup is a recent addition and is not something the team depends on yet.
16
-
17
- ---
18
-
19
- ## What this change does and why
20
-
21
- The selfie screen used to hang forever if the face model download never finished: the user was left on a spinner with no message and no way out. This change makes the download cancellable, adds a 20-second timer that notices when it is taking too long, and adds two pop-ups: one saying "still starting your camera" while the download is still alive, and one saying "couldn't set up the camera" with a Retry button when it has died. The pop-up component was also tidied up to take named settings instead of a long list of unlabelled arguments.
22
-
23
- Nothing serious is left. What remains is one write-up that no longer matches the code, and three small tidy-ups.
24
-
25
- ---
26
-
27
- ## Minor
28
-
29
- **1.** The merge request title and write-up
30
-
31
- The title and description say the face download now times out and retries by itself with increasing waits. It doesn't: it tries once, then after 20 seconds offers the user a "Restart camera" button. One of the new tests even checks that there is no automatic retry. Whoever verifies this will go looking for retry behaviour that isn't there and may report it as broken. Fix: update the title and description to describe what actually shipped.
32
-
33
- **2.** [src/classes/MainPage.ts:104-105](src/classes/MainPage.ts#L104-L105)
34
-
35
- Two pieces of on-screen wording are still typed directly into the code instead of being kept in the central wording list with everything else, and one of them is the "you have not submitted your documents" warning. If a second language is ever added, these stay stuck in English and nothing would warn anyone. The matching problem in the shared pop-up file was fixed in the newest commit; this is the leftover. Fix: move both into the central wording list.
36
-
37
- **3.** [src/plugins/card-camera.ts:250-253](src/plugins/card-camera.ts#L250-L253)
38
-
39
- One block of the rewritten code is still at the wrong indentation, so it looks different from the identical block further down at [src/plugins/card-camera.ts:528-532](src/plugins/card-camera.ts#L528-L532). Harmless, but it makes a reader stop and double-check the two are the same. Unchanged since the first round. Fix: line those lines up by hand. Not with an automatic formatter: this project has never been formatted that way, so running one would change a lot of unrelated lines and hide the actual fix.
40
-
41
- ---
42
-
43
- ## Enhancement
44
-
45
- **4.** [src/plugins/face-detection.ts:859-862](src/plugins/face-detection.ts#L859-L862)
46
-
47
- When the phone's browser cancels the download by itself, the code now correctly turns that into a normal failure so the user gets a Retry button. Good, but in doing so it throws away the original error details. That situation only happens on a real phone, and this logging is the main way to work out what went wrong, so throwing the details away removes the only clue. Fix: keep the original error alongside the new one when logging.
@@ -1,71 +0,0 @@
1
- # eKYC Web UI review — sc-356160 (fix face model-load deadlock, add stall-recovery UX)
2
-
3
- **Counts:** 0 Critical · 0 Major · 3 Minor · 1 Enhancement · **4 total**
4
-
5
- Project: repo `pw-ekyc-web-ui`, npm package `pipwave-ekyc-uikit`.
6
- Not to be confused with the **eKYC Web SDK** (repo `pw-ekyc-web-sdk`, package `pipwave-ekyc-sdk`), which has its own `uisdk.ts` / `PWUISDK` entry point.
7
-
8
- Branch: `hotfix-teojunjie-sc-356160-fix-face-model-load-deadlock`
9
- MR: https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/merge_requests/94 (target `release-beta`)
10
- Reviewed at: `0f1a50ccd1b1aec5b54c0d414f20372396cc3a12`
11
- Compared against: `origin/release-beta` (merge base `4ab693b`)
12
-
13
- **Round 2.** All 13 threads from round 1 are resolved, and commit `0f1a50c` ("Rename dialog flag, i18n cancel fallback, drop unused jsdom") lands the fixes. Findings 1, 2, 3 and 5 from round 1 are fixed and dropped. Finding 4 is half fixed and survives as finding 2 below; finding 6 is untouched and survives as finding 3. Two new findings come out of the fix commit itself.
14
-
15
- **Scope note:** findings about the Vitest setup itself (coverage thresholds, `.nvmrc` / `engines` for the test toolchain) were reviewed and deliberately excluded — Vitest is a recent addition, not part of the team's established toolchain, and is not being held to a release gate.
16
-
17
- ---
18
-
19
- ## What this change does and why
20
-
21
- The selfie screen could hang forever. The old model loader parked callers in a `while (state === "LOADING") { sleep(500) }` spin, so if the face-api download never finished, nothing ever broke the loop, no error surfaced, and the user sat on a spinner with no way out. This change replaces that with a proper load state machine: one shared abortable load promise, an `AbortController` threaded into both face-api's manifest fetch and TensorFlow.js's weight-shard fetch, a 20-second stall watchdog, and two new selfie dialogs (a "still starting your camera" countdown card while the download is alive, and a "couldn't set up the camera / Retry" dialog when it dies). Bundled alongside is a mechanical refactor of `BaseDialog.render(...)` from eight positional arguments to a single options object, applied to every call site.
22
-
23
- Round 1's two Major findings were the two outcomes the new state machine could not distinguish — an abort raised by the browser rather than by `abortLoad()`, and a fetch patch that only half-installed. Both are now closed properly rather than papered over: the catch block stops keying on `isAbortError` and reshapes a self-abort into a plain failure so `init()` surfaces Retry, and the single `fetchPatched` latch is split into `manifestFetchPatched` / `shardFetchPatched` so a half-installed patch retries on the next attempt. Both fixes carry tests that fail against the old code.
24
-
25
- What remains is the tail of that work rather than anything structural: the fix commit's error reshaping loses the original abort on the way out, and the MR description still describes an earlier design (`withTimeout` plus backoff) that the shipped face path does not use, which matters because the four-eyes verifier tests against that description. The other two are leftovers from the dialog refactor that round 1 flagged and this commit only partly picked up.
26
-
27
- Verified as correct on this revision: the round-1 fixes hold — `abortLoad()` nulls `loadController` synchronously before the rejection lands, so the `loadController !== controller` check now means only "superseded"; `init()` at [face-detection.ts:964](src/plugins/face-detection.ts#L964) sees the reshaped error as a non-abort and does clear the watchdog, hide the spinner and call `onModelLoadFailed?.()`; `loadPromise` is cleared in a `finally`, so a rejected attempt is never cached and re-served; `patchFaceApiFetch()` cannot double-wrap either fetch, and face-api's `env.initialize()` (which replaces the env object) runs only at module load and in the repo's own `getEnv()` catch, so the split flags cannot end up pointing at a stale env object; `btn_cancel` exists in `en.ts`; the new `translations` import in `baseDialog.ts` introduces no cycle (`translations/index.ts` imports only `./en`); `computeBackoffMs` / `withTimeout` are still live via the card path, so nothing is left dead; and `tsc --noEmit` is clean. The test suite could not be executed here — local Node is v16 and Vitest 4's rolldown requires `node:util`'s `styleText` (Node 20+) — so the new tests were read rather than run.
28
-
29
- ---
30
-
31
- ## Minor
32
-
33
- ### 1. The MR description and title describe timeout-and-backoff retry the face load doesn't do
34
-
35
- The title reads "Add timeout and retry to face model load" and the description says "Bound each load attempt with `withTimeout` and retry with backoff". The shipped face path does neither: `_attemptLoadModel` makes exactly one attempt with no timeout, and recovery is the 20s stall watchdog plus a user-driven "Restart camera". `withTimeout` and `computeBackoffMs` are imported only by `card-detection.ts`, never by `face-detection.ts`, and a test in this MR asserts the opposite of the description — "makes exactly ONE attempt on failure — no auto-retry".
36
-
37
- The description is what the four-eyes verifier and QA test against, so it sends them looking for automatic retries and backoff timing that don't exist, and a missing auto-retry reads as a bug rather than the design.
38
-
39
- Suggest updating the description (and title) to describe the watchdog-plus-manual-Restart design that shipped, or noting explicitly that the timeout/backoff helpers cover the card path only.
40
-
41
- ### 2. Hardcoded user-facing strings left at a dialog call site this change rewrites
42
-
43
- [src/classes/MainPage.ts:104-105](src/classes/MainPage.ts#L104-L105)
44
-
45
- The `baseDialog` half of round 1's finding is fixed — `createSecondaryButton(cancelText || $t("btn_cancel"))` now goes through `$t`. This pair does not: `text: "You have not submitted your documents..."` and `confirmText: "Leave now"` are still literal, while every other dialog in the branch pulls copy from `$t(...)`. They pre-date the branch and are in scope only because the positional-to-options conversion already rewrites these exact lines.
46
-
47
- `$t` resolves against a `mapper` holding only `en`, with `state.locale` already settable. When a second locale lands these stay English inside an otherwise-translated dialog, with nothing in the build to flag it — and this one is the "leave without saving" confirmation.
48
-
49
- Suggest adding `leave_page_title` and `btn_leave_now` to `en.ts` and using them here. Note `$t` returns `""` for an unknown key rather than throwing, so a typo renders blank. If pre-existing copy is out of scope for a hotfix, this can follow up separately now that the new-code half is closed.
50
-
51
- ### 3. Indentation still lost on one converted dialog call site
52
-
53
- [src/plugins/card-camera.ts:250-253](src/plugins/card-camera.ts#L250-L253)
54
-
55
- The object literal passed to `idleDialog.render({...})` kept its old outer indentation and sits two levels short of its block. The identical conversion at [line 528](src/plugins/card-camera.ts#L528) is correct, so the two read as different code when they are the same call. Unchanged since round 1.
56
-
57
- Suggest re-indenting these four lines by hand to match line 528. Worth doing manually rather than through a formatter: the project has no committed formatting baseline, so running one over the file would rewrite far more than these lines and bury the fix.
58
-
59
- ---
60
-
61
- ## Enhancement
62
-
63
- ### 4. The self-abort reshape discards the original abort error
64
-
65
- [src/plugins/face-detection.ts:859-862](src/plugins/face-detection.ts#L859-L862)
66
-
67
- Reshaping a browser self-abort into `new Error("face-model-load-self-aborted")` is what makes `init()` surface Retry, so the mechanism is right. But the original error is dropped entirely: the `faceLog` line above it logs only a message, and `init()`'s `faceLog("init aborted: face model load failed", err)` then logs the synthetic error, so the browser's own abort message and stack never reach the log.
68
-
69
- This is the one failure mode that only reproduces on a real device under backgrounding or memory pressure, and it is exactly what `window.__PW_FACE_DEBUG__` logging exists to diagnose. Losing the underlying error removes the only evidence distinguishing "iOS cancelled the fetch" from any other abort source.
70
-
71
- Suggest passing `err` into the `faceLog` call so it survives in the on-device log, or carrying it on the thrown error with `Object.assign(new Error("face-model-load-self-aborted"), { cause: err })`. Note the two-argument `new Error(msg, { cause })` form does not typecheck here — `tsconfig.json` targets `es2016`, so the `cause` option isn't in the lib.