pipwave-ekyc-uikit 3.6.0-beta.1 → 3.6.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,16 @@
1
+ # Release drafts: pipwave-ekyc-uikit 3.6.0
2
+
3
+ ## #hello-serverless (post when the release MR is ready to merge)
4
+
5
+ ```
6
+ pipwave eKYC Web UI Release 3.6 is ready
7
+ • <https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/merge_requests/100|MR>
8
+ • <https://gitlab.g2g.com/pipwave-frontend/pw-ekyc-web-ui/-/wikis/**Release-3.6-Deployment-Notes#release-36|Release Notes>
9
+ @Jia Chong Ong
10
+ ```
11
+
12
+ Thread reply when the deploy starts:
13
+
14
+ ```
15
+ deploying
16
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipwave-ekyc-uikit",
3
- "version": "3.6.0-beta.1",
3
+ "version": "3.6.0",
4
4
  "description": "",
5
5
  "main": "dist/pw-bundle.js",
6
6
  "module": "dist/pw-bundle.mjs",
@@ -1,55 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(grep -o \"facingMode[^,]*,width:{ideal:[0-9]*},height:{ideal:[0-9]*}\" dist/pw-bundle.js)",
5
- "Bash(grep -o 'facingMode:\"environment\",width:{ideal:[0-9]*},height:{ideal:[0-9]*}')",
6
- "Bash(npm whoami *)",
7
- "Bash(npm view *)",
8
- "Bash(git -C \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui\" status --porcelain)",
9
- "Bash(echo \"---exit:$?\")",
10
- "Bash(ls -d /Applications/*.app 2>/dev/null | grep -iE \"visual|code|cursor|windsurf\"; echo \"---\"; ls \"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code\" 2>/dev/null && echo \"VSCODE_CLI_FOUND\")",
11
- "Read(//Applications/**)",
12
- "Read(//Applications/Visual Studio Code.app/Contents/Resources/app/bin/**)",
13
- "Bash(ps -Ao comm=)",
14
- "Bash(\"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code\" -r ~/.claude/skills/release-uikit/SKILL.md ~/.claude/skills/rollout-uikit/SKILL.md)",
15
- "Bash(mv release-uikit *)",
16
- "Bash(mv rollout-uikit *)",
17
- "Bash(\"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code\" -r ~/.claude/skills/release-ekyc-uikit/SKILL.md ~/.claude/skills/rollout-ekyc-uikit/SKILL.md)",
18
- "Bash(rm -rf ~/.claude/skills/rollout-ekyc-uikit && echo \"deleted\" && ls ~/.claude/skills && echo \"=== stale refs? ===\" && grep -rn \"rollout-ekyc-uikit\" ~/.claude/skills 2>/dev/null || echo \"none\")",
19
- "Edit(~/.claude/skills/release-ekyc-uikit/**)",
20
- "Bash(node -p 'require\\('\\\\''/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/package.json'\\\\''\\).name')",
21
- "Bash(node -p 'require\\('\\\\''/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/package.json'\\\\''\\).version')",
22
- "Bash(git -C '/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui' rev-parse --abbrev-ref HEAD)",
23
- "Bash(git -C '/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui' status --porcelain)",
24
- "Bash(npm -v)",
25
- "Bash(node -e ' *)",
26
- "Bash(grep -o 'facingMode:\"environment\",width:{ideal:[0-9]*},height:{ideal:[0-9]*}' dist/pw-bundle.js)",
27
- "Bash(grep -o 'facingMode:\"user\",width:{ideal:[0-9]*},height:{ideal:[0-9]*}' dist/pw-bundle.js)",
28
- "Bash(sed -E 's/\\(=.{4}\\).*/\\\\1****REDACTED****/' .env.development)",
29
- "Bash(sed -E 's/\\(=.{4}\\).*/\\\\1****REDACTED****/' .env.production)",
30
- "Bash(node -p \"Object.keys\\(require\\('./.claude/settings.local.json'\\)\\).join\\(', '\\)\")",
31
- "Bash(grep -n \"Remove the \\\\`face-api.js\\\\` dependency\\\\|face-api.js\\\\`, \\\\`public/face-models/\\\\`\\\\|minScore\\\\|livenessScore\\\\|Within scope\\\\|declare/require\" docs/stories/e-sc-354880-signal-package-adapter-and-geometry.md)",
32
- "Bash(echo \"\\(exit $?\\)\")",
33
- "Bash(awk '/^## Description/,0' docs/stories/tickets/e-sc-354880-signal-package-adapter-and-geometry.md)",
34
- "Bash(awk '{print length\"\\\\t\"NR\"\\\\t\"substr\\($0,1,90\\)}' docs/stories/tickets/e-sc-354880-signal-package-adapter-and-geometry.md)",
35
- "Bash(awk '/^# Documents/,0' docs/stories/tickets/e-sc-354880-signal-package-adapter-and-geometry.md)",
36
- "Bash(git diff *)",
37
- "Bash(grep -rn \"Mapping \\\\`14025\\\\`\" stories/)",
38
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs\" && grep -n \"^## 7\\\\.0\\\\|^### \\\\|^## 7\" plan-epic-342056-selfie-action-checking.md | sed -n '1,40p' && echo \"=== §7.0 blocker table + contract items ===\" && sed -n '/^## 7.0/,/^## 7.1\\\\|^## 8/p' plan-epic-342056-selfie-action-checking.md | head -60)",
39
- "Bash(awk '/^### |^## /{sec=$0} /livenessScore|liveness_score/{printf \"%-56s L%-5s %s\\\\n\", substr\\(sec,1,54\\), NR, substr\\($0,1,90\\)}' plan-epic-342056-selfie-action-checking.md)",
40
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs\" && echo \"=== §6.3 sequencing \\(first 30 lines\\) ===\" && sed -n '/^### 6.3 Sequencing/,/^### 6.4/p' plan-epic-342056-selfie-action-checking.md | head -32 *)",
41
- "Bash(awk 'NR>=30 && NR<=42 {printf \"%d:[%s]\\\\n\", NR, $0}' a-sc-353927-web-sdk-facematch-selfie-fields.md)",
42
- "Bash(perl -0777 -i -pe 's{\\\\*\\\\*Signal-package adapter \\\\+ phase-1 geometry\\\\*\\\\*; delete face-api\\\\.js\\\\.}{**Signal-package adapter + phase-1 geometry**; delete face-api.js **and the `livenessScore` chain, incl. `StartSelfie.ts:137,142` \\(§3 item 5\\)**.}' plan-epic-342056-selfie-action-checking.md)",
43
- "Bash(perl -0777 -i -pe 's{outside this story\\\\.\\\\n\\\\n\\\\n# Acceptance Criteria}{outside this story.\\\\n\\\\n# Acceptance Criteria}' stories/a-sc-353927-web-sdk-facematch-selfie-fields.md)",
44
- "Bash(perl -0777 -ne 'print \" found\\\\n\" if /\\\\n\\\\n\\\\n/' stories/a-sc-353927-web-sdk-facematch-selfie-fields.md)",
45
- "Bash(python3 -c ' *)",
46
- "Bash(grep -rn 'export.*\\\\$t\\\\|const $t\\\\|function $t' src/)",
47
- "Bash(node -e \"const p=require\\('./package.json'\\);console.log\\(p.name,p.version,'private:',p.private,'main:',p.main,'files:',JSON.stringify\\(p.files\\)\\)\")"
48
- ],
49
- "additionalDirectories": [
50
- "/Users/chechin/.claude/skills/rollout-ekyc-uikit",
51
- "/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs",
52
- "/Users/chechin/.claude"
53
- ]
54
- }
55
- }
@@ -1,61 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(xargs git log --oneline -1)",
5
- "Skill(code-review)",
6
- "Skill(code-review:*)",
7
- "Bash(kill 60053 69150 84076)",
8
- "Bash(lsof -iTCP -sTCP:LISTEN -P -n)",
9
- "Bash(npx vitest *)",
10
- "Bash(git -C \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui\" remote -v)",
11
- "Bash(ssh-keygen -t ed25519 -C \"chechin.tang@g2g.com\" -f ~/.ssh/id_ed25519_gitlab_g2g -N \"\")",
12
- "Bash(pbcopy)",
13
- "Bash(pbpaste)",
14
- "Bash(ssh -o BatchMode=yes -T git@gitlab.g2g.com)",
15
- "Bash(ssh -v -T git@gitlab.g2g.com)",
16
- "Bash(git log *)",
17
- "Bash(lsof -nP -iTCP -sTCP:LISTEN)",
18
- "Bash(node -e ' *)",
19
- "Bash(node -e \"const p=require\\('./package.json'\\);console.log\\(JSON.stringify\\(p.scripts,null,2\\)\\);console.log\\('files:',p.files,'main:',p.main\\)\")",
20
- "Bash(ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate -of default=noprint_wrappers=1 \"/Users/chechin/Desktop/Screen Recording 2026-08-12 at 7.27.54 AM.mov\")",
21
- "Bash(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=noprint_wrappers=1 \"/Users/chechin/Desktop/Screen Recording 2026-08-12 at 7.27.54 AM.mov\")",
22
- "Bash(grep -i -E \"\\\\.\\(mov|mp4|m4v\\)$\")",
23
- "Bash(~/.claude/skills/compress-video/compress-video.sh --help)",
24
- "mcp__claude_ai_G2G_Shortcut__stories-get-by-id",
25
- "Bash(python3 -c ' *)",
26
- "Bash(python3 -)",
27
- "mcp__claude_ai_Figma__get_screenshot",
28
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui\" && sed -n '/^### Overlap resolution/,/^---/p' docs/plan-epic-342056-selfie-action-checking.md)",
29
- "Bash(mv \"support-facematch-selfie-fields-in-web-sdk.md\" \"a-support-facematch-selfie-fields-in-web-sdk.md\")",
30
- "Bash(mv \"selfie-flow-and-orchestration.md\" \"b-selfie-flow-and-orchestration.md\")",
31
- "Bash(python3 -c \"print\\('A .5 + B1 2.5 + C 3 + D 3 + E 3.25 + F 5.25 + G 1.5 + B2 3 =', 0.5+2.5+3+3+3.25+5.25+1.5+3\\)\")",
32
- "Bash(grep -n \"story \\\\*\\\\*B\\\\*\\\\*\\\\|\\(B\\)\\\\b\\\\|B's mocks\\\\|B's debug\\\\|B's \\\\`\" docs/plan-epic-342056-selfie-action-checking.md)",
33
- "Bash(grep -n \"^#\\\\{2,4\\\\} \" plan-epic-342056-selfie-action-checking.md)",
34
- "Bash(grep -v \"are$\\\\|dropped\\\\|superseded\")",
35
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs\" && echo \"=== §7.0 refs ===\" && sed -n '1532,1533p' plan-epic-342056-selfie-action-checking.md | grep -o \"\\\\*\\\\*[FG]\\\\*\\\\*[^.]*\"; echo; echo \"=== §6.3 sequencing prose ===\" && sed -n '/^### 6.3 Sequencing/,/^### 6.4/p' plan-epic-342056-selfie-action-checking.md | grep -n \"\\\\*\\\\*[FG]\\\\*\\\\*\\\\|── [FG] \\\\|[FG] visibility\\\\|[FG] sequencer\" | sed 's/\\\\\\(.\\\\{130\\\\}\\\\\\).*/\\\\1…/')",
36
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs\" && sed -n '/^### 6.3 Sequencing/,/^### 6.4/p' plan-epic-342056-selfie-action-checking.md | grep -n \"\\\\bF\\\\b\\\\|\\\\bG\\\\b\" | sed 's/\\\\\\(.\\\\{150\\\\}\\\\\\).*/\\\\1…/')",
37
- "mcp__claude_ai_G2G_Shortcut__stories-search",
38
- "mcp__claude_ai_Figma__get_metadata",
39
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs\" && sed -n '/Invalidate with a dirty flag/,/^### /p' plan-epic-342056-selfie-action-checking.md | sed -n '8,30p')",
40
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs/..\" && grep -rn \"requestAnimationFrame\\\\|cancelAnimationFrame\" src/plugins/*.ts | sed 's/\\\\\\(.\\\\{110\\\\}\\\\\\).*/\\\\1…/')",
41
- "Read(//Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/**)",
42
- "Bash(— a *)",
43
- "Bash(grep -n \"config.ts` — a skeleton\\\\|config.ts` — skeleton\" stories/b-selfie-flow-scaffold.md plan-epic-342056-selfie-action-checking.md)",
44
- "Bash(cd \"/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui\" && grep -n \"^_Rev 2[4-9]\" docs/plan-epic-342056-selfie-action-checking.md | head && sed -n '/^_Rev 26/,/^_Rev 25/p' docs/plan-epic-342056-selfie-action-checking.md | head -30)",
45
- "Bash(awk '/^\\\\| \\\\| Shortcut \\\\| Draft/,/^$/' docs/plan-epic-342056-selfie-action-checking.md)",
46
- "Bash(awk -F'|' '{print NF-2\" cols: \"$2}')",
47
- "Bash(perl -i -pe 'if \\(/^_/\\) { s/the flow scaffold story/the flow scaffold story \\(sc-354838\\)/; s/the flow scaffold\\(?! story\\)\\(?! \\\\\\(sc\\)/the flow scaffold \\(sc-354838\\)/; }' docs/stories/c-selfie-camera-screen-ui.md docs/stories/d-selfie-non-camera-screens.md docs/stories/f-visibility-prompts-and-fallback.md docs/stories/h-selfie-submission-and-result.md docs/stories/tickets/c-selfie-camera-screen-ui.md docs/stories/tickets/d-selfie-non-camera-screens.md docs/stories/tickets/f-visibility-prompts-and-fallback.md docs/stories/tickets/h-selfie-submission-and-result.md)",
48
- "Bash(perl -i -pe 's/^As a Frontend Developer, I want the SDK package to accept the facematch selfie fields, so that the UI can submit action-stage images\\\\.$/As a Frontend Developer, I want the pw-ekyc-web-sdk to accept the facematch selfie fields, so that the pw-ekyc-web-ui can submit action-stage images./' docs/stories/a-support-facematch-selfie-fields-in-web-sdk.md docs/stories/tickets/a-web-sdk-facematch-selfie-fields.md)",
49
- "Bash(awk '/^# Documents$/{f=1} f' e-signal-package-adapter-and-geometry.md)",
50
- "Bash(awk '/^# Documents$/{f=1} f' g-action-sequencer-and-capture.md)",
51
- "Bash(perl -0777 -i -pe ' *)",
52
- "Bash(perl -0777 -i -pe 's/- Only one image is captured and uploaded; the new flow sends three\\\\./- Only one image is captured and uploaded; the new flow sends 3./' g-action-sequencer-and-capture.md)",
53
- "mcp__claude_ai_G2G_Shortcut__stories-create",
54
- "mcp__claude_ai_G2G_Shortcut__stories-update",
55
- "Bash(set -e)"
56
- ],
57
- "additionalDirectories": [
58
- "/Users/chechin/Documents/2. pipwave Projects/pw-ekyc-web-ui /pw-ekyc-web-ui/docs"
59
- ]
60
- }
61
- }
@@ -1,184 +0,0 @@
1
- # Branch review: `hotfix-teojunjie-sc-356160-fix-face-model-load-deadlock`
2
-
3
- Compared against `origin/master`. The branch is 2 commits ahead, 0 behind.
4
-
5
- - `1cb8851` [sc-356160] Add timeout and retry to face model load
6
- - `09fb8c1` [sc-356160] Add Retry dialog when face model load fails
7
-
8
- ```
9
- src/plugins/face-camera.ts | 16 ++
10
- src/plugins/face-detection.ts | 80 ++++--
11
- src/plugins/test/face-detection.load.test.ts | 221 +++++++++++++++++ (new)
12
- src/plugins/{ => test}/card-detection.inference.test.ts
13
- src/plugins/{ => test}/card-detection.test.ts
14
- src/plugins/{ => test}/detection-failure.test.ts
15
- src/plugins/{ => test}/face-detection.test.ts
16
- src/translations/en.ts | 1 +
17
- 8 files changed, 308 insertions(+), 30 deletions(-)
18
- ```
19
-
20
- ## The bug being fixed
21
-
22
- The selfie screen downloads face-api model files before it can start. The old code in
23
- `src/plugins/face-detection.ts` did this:
24
-
25
- 1. Set `loadedModelState` to `"LOADING"`.
26
- 2. Start the download.
27
- 3. Wait in a loop: `while (loadedModelState === "LOADING") { sleep 500ms }`.
28
-
29
- If a download stalled but never actually failed, step 1's `"LOADING"` value was never changed. So the loop
30
- in step 3 kept waiting forever, and the user was stuck looking at the "Initializing Camera..." spinner with
31
- no error and no way forward.
32
-
33
- ## What the branch changes
34
-
35
- ### 1. `src/plugins/face-detection.ts` — bound the download and retry it
36
-
37
- - **One shared download.** A new module-level `loadPromise` holds the single download in progress. The
38
- background pre-warm (from `src/uisdk.ts`) and the selfie screen now wait on the same promise instead of
39
- each watching the `loadedModelState` variable. It is set back to `null` when the download finishes, so a
40
- failed download can be tried again later.
41
- - **A time limit and retries.** The new `_attemptLoadModel()` wraps the four loader calls in
42
- `withTimeout(..., 10000, "face model load")` and retries up to 2 more times, waiting 500ms then 1s between
43
- tries. If all 3 attempts fail, it sets `loadedModelState` back to `"NOT_LOADED"` and throws.
44
- - **`init()` handles the failure.** It now wraps the download in try/catch. On failure it hides the spinner,
45
- calls a new `onModelLoadFailed?.()` callback, and returns without starting detection. The old endless
46
- `while` loop is gone.
47
- - **Reuses existing helpers.** `withTimeout` and `computeBackoffMs` already existed in
48
- `src/plugins/detection-failure.ts` (written for the card model loader), so nothing was duplicated. It
49
- skips the third helper, `classifyFailure`, and the comment explains why: face-api's loaders only download
50
- files, so every failure here is worth retrying.
51
-
52
- ### 2. `src/plugins/face-camera.ts` — show a Retry dialog
53
-
54
- `init()` now sets `faceDetectionSdk.onModelLoadFailed` to a new `showModelLoadFailedDialog()` method. That
55
- method opens a `BaseDialog` with the message `$t("face_camera_unavailable")` and a `$t("btn_retry")` button,
56
- plus a Cancel button.
57
-
58
- Pressing Retry calls `faceDetectionSdk.init()` again. That works because the state was already reset to
59
- `"NOT_LOADED"`, so it starts a genuinely new download instead of waiting on the dead one. Cancel just closes
60
- the dialog and leaves the back button in the header as the way out.
61
-
62
- ### 3. `src/translations/en.ts` — one new string
63
-
64
- `face_camera_unavailable: "Couldn't start the camera. Please check your connection and retry."`
65
-
66
- `en.ts` is the only translation file in this repo, so there are no other languages left missing the string.
67
-
68
- ### 4. Tests moved and added
69
-
70
- Four existing test files moved from `src/plugins/` into `src/plugins/test/`. I checked the diff: the only
71
- content change is the import paths getting one level deeper (`./card-detection` became `../card-detection`,
72
- `../classes/MainPage` became `../../classes/MainPage`). No config change was needed because
73
- `vitest.config.ts` already looks for `src/**/*.test.ts`.
74
-
75
- `face-detection.load.test.ts` is new and adds 9 tests:
76
-
77
- - a normal successful load
78
- - a load that fails once then succeeds on retry
79
- - a stalled load that times out, fails, and resets the state cleanly
80
- - a fresh retry working after an earlier failure
81
- - two callers at once sharing a single download (not downloading twice)
82
- - the failure reaching the second caller too, so it isn't left hanging
83
- - a download that finishes *after* its attempt already timed out, which must be ignored
84
- - `init()` hiding the spinner when the load fails
85
- - `init()` calling `onModelLoadFailed` so the dialog can appear
86
-
87
- ## Checks I ran
88
-
89
- | Check | Result |
90
- | --- | --- |
91
- | `npx vitest run` | 5 files, **76 tests passed** |
92
- | `npx tsc --noEmit` | no errors |
93
- | `npm run build` | builds fine; only the bundle-size warnings that were already there |
94
-
95
- One thing to watch: the tests need Node 18 or newer. On Node 16 (the shell default here) vitest crashes at
96
- startup with a `node:util` / `styleText` SyntaxError before running a single test. Worth checking that CI and
97
- other developers aren't on Node 16.
98
-
99
- ## Problems found
100
-
101
- ### 1. The 10-second limit is probably too short (most important)
102
-
103
- The four loaders download about 7 MB in total, from `public/face-models`:
104
-
105
- | file | size |
106
- | --- | --- |
107
- | `tiny_face_detector_model-shard1` | 193 KB |
108
- | `face_landmark_68_model-shard1` | 357 KB |
109
- | `face_expression_model-shard1` | 329 KB |
110
- | `face_recognition_model-shard1` + `shard2` | 6.44 MB |
111
- | **total** | **~7.0 MB** |
112
-
113
- To finish 7 MB in 10 seconds you need about 700 KB/s, which is roughly 5.6 Mbps held steady. On a weak
114
- mobile connection the download is fine, just slow, but the new code gives up on it. It then retries twice,
115
- also giving up, and shows "Couldn't start the camera" after about 31.5 seconds (three 10s attempts plus
116
- 500ms and 1s of waiting).
117
-
118
- So we've fixed a rare freeze but made slow connections fail where they used to eventually work.
119
-
120
- Two smaller side effects of the same setting:
121
-
122
- - `withTimeout` doesn't cancel the download it gave up on. Sometimes that helps, because the first download
123
- may finish and fill the browser cache in time for retry 2 or 3 to reuse it. But that depends on the cache
124
- headers on `assets.pipwave.com` and on lucky timing, so we can't rely on it.
125
- - Because nothing is cancelled, up to 3 attempts x 4 loaders can be downloading at the same time, all
126
- fighting over the same slow connection.
127
-
128
- What I'd suggest, best option first:
129
-
130
- 1. Raise the limit per attempt to 30-45 seconds. The point is to catch a download that has stopped moving,
131
- not one that is merely slow, so the limit doesn't need to be tight.
132
- 2. Or detect a stall instead of a total time (give up only when no bytes have arrived for a while).
133
- 3. Or pass an `AbortController` so a given-up attempt stops using bandwidth. face-api's loaders may not
134
- accept one, in which case option 1 matters more.
135
-
136
- Worth noting this mostly affects the slow-connection minority: the pre-warm in `src/uisdk.ts:83` starts
137
- downloading early in the eKYC flow, so most users are done well before they reach the selfie screen.
138
-
139
- ### 2. The 90-second idle timer can fire while the Retry dialog is open
140
-
141
- `FaceCamera.init()` starts a 90-second idle timer (`src/plugins/face-camera.ts:233-245`) before the model
142
- download begins. One failed download cycle uses up to 31.5 seconds of that.
143
-
144
- So if the user reads the dialog and taps Retry late, the idle timer can go off in the middle of the retry.
145
- The timer calls `stopVideoStream()`, which kills the camera tracks, while the retrying `init()` carries on
146
- and calls `startDetection()` on a camera that no longer works. The result is a stuck camera with no spinner
147
- and no error message.
148
-
149
- Simple fix: reset the idle timer inside the Retry callback (or clear it when the load fails and restart it
150
- on Retry).
151
-
152
- ### 3. Two dialogs could stack up (minor)
153
-
154
- `init()` doesn't check whether it is already running before starting the download. The
155
- `!this.iFrameRef && !this.isStartingDetection` check happens after the `await`, not before it. If the video
156
- element fires `loadedmetadata` twice (for example the camera stream is re-acquired, or the device rotates),
157
- both calls share the same failing download and both call `onModelLoadFailed`, so two dialogs appear on top
158
- of each other. A simple "already showed the dialog" flag would prevent it.
159
-
160
- ### 4. Cosmetic
161
-
162
- - `src/plugins/face-detection.ts:745` has trailing whitespace on a new blank line inside
163
- `_attemptLoadModel()`. Two similar cases were already in the file. There's no eslint config in this repo,
164
- so nothing flags it.
165
- - The Cancel button text is hardcoded as `"Cancel"` in `src/components/baseDialog.ts:73` instead of using
166
- `$t(...)`. This isn't new, and it doesn't matter while English is the only locale, but this new dialog is
167
- one more place using it.
168
-
169
- ## Things I checked that are correct
170
-
171
- - **No way back into the old freeze.** There is never a moment where `loadedModelState` is `"LOADING"` while
172
- `loadPromise` is `null`. An async function runs up to its first `await` immediately, so `"LOADING"` is set
173
- before the promise is handed to anyone, and on failure `"NOT_LOADED"` is set before the throw. So a caller
174
- arriving at any point either joins the live download or starts a new one.
175
- - **No unhandled promise rejections.** The caller that starts the download always awaits it, and the
176
- pre-warm at `src/uisdk.ts:83` has a `.catch(...)`.
177
- - **Retry really does start over.** The state is reset and `loadPromise` cleared before the dialog appears,
178
- so Retry downloads again rather than waiting on the failed promise. Test 4 covers this.
179
- - **Retry shows a spinner.** `FACEDETECTIONSDK.init()` calls `createFullLoading` itself, so pressing Retry
180
- gives visible feedback instead of looking like nothing happened.
181
- - **The `finally` block in `FaceCamera.init()`** still hides the spinner based on
182
- `getLoadedModelState() !== "LOADING"`. This branch doesn't change that behaviour.
183
- - **`dist/pw-bundle.js` doesn't include these changes.** That's expected: this repo only commits `dist/` when
184
- the version is bumped for a release.
@@ -1,88 +0,0 @@
1
- **Code review notes**
2
-
3
- 18 findings from reviewing `2012a78` and `f647878`, grouped by urgency. Files covered: `card-camera.ts`, `VerifyId.ts`, `manualCapture.ts`, `manualCapture.test.ts`, `en.ts`.
4
-
5
- ---
6
-
7
- ### Must fix before merge
8
-
9
- **1. One failed tap permanently locks the shutter** — `card-camera.ts:627-637`
10
- `triggerManualCapture()` sets `hasAutoCaptured = true` and locks the button before awaiting `captureImage()`, with no try/catch. If the capture throws (for example iOS Safari after backgrounding, where the video reports width 0) the error disappears, the button stays locked, and manual mode has no other way to capture.
11
- Fix: wrap it in try/catch. On error, reset the flag, unlock the button, and restore the hint.
12
-
13
- ---
14
-
15
- ### To confirm and resolve (no code change expected)
16
-
17
- **2. The capture is still cropped to the card-ratio frame** — `card-camera.ts:111`, ratio from `src/constant/id.ts:27`
18
- The overlay frame and `captureImage()` crop still use the credit-card ratio (1.586), so the UA booklet has to be fitted into a card-shaped frame. Understanding is this is expected: we cannot define a different ratio for every doc type per country/region, and since capture is manual, the user adjusts the document to fit the frame themselves. Please confirm and resolve.
19
-
20
- **3. The flow still asks for a "back side" of the booklet** — `VerifyId.ts:282`, `idCardTypeMapping` says national ID has a back
21
- After the front capture the user gets the "flip your card" step for a document with no card back. Nothing to change in this MR, since BE currently enforces submission of both sides. Reminder for the future: consider a BE-side change to make the back side optional for booklet-style documents, subject to business requirements. Please acknowledge and resolve.
22
-
23
- ---
24
-
25
- ### Recommended (worth doing in this MR)
26
-
27
- **4. Tapping the shutter gives no feedback** — `src/plugins/card-camera.ts:627-637`
28
- Auto capture shows the ring filling and a green tick; manual capture shows nothing until the loading screen. Users will assume the button is broken and tap again. This was a rare fallback before, but it is now the only path for UA. Suggest reusing `setRingProgress(1)` + `showCaptureTick()` in `triggerManualCapture`.
29
-
30
- **5. Scanning brackets show while nothing is scanning** — `src/plugins/card-camera.ts:928`
31
- The detector corner brackets still render in manual mode but never change colour, since no detection runs. It reads as "the app is scanning", so the user may wait instead of tapping. `disableDetection()` already exists and the face camera uses it for exactly this.
32
-
33
- **6. The same hint ternary is copy-pasted in three places** — `card-camera.ts:402`, `:588`, `:798`
34
- `$t(this.manualOnly ? "doc_manual_capture" : "doc_align_card")` appears in three methods. The next person resetting the hint will likely copy nearby code and write `$t("doc_align_card")`, telling UA users to "align your card" with no auto-capture running. One getter used at all three sites:
35
-
36
- ```ts
37
- private get idleHint(): string {
38
- return $t(this.manualOnly ? "doc_manual_capture" : "doc_align_card");
39
- }
40
- ```
41
-
42
- **7. Two separate lists will break when we add a second country** — `src/helper/manualCapture.ts:9-13`
43
- The check is "country in list A AND doc type in list B". The real rule is per-country ("UA's national ID is a booklet"). If we later add a country whose passport is the booklet, adding `PASSPORT` to the shared list would silently enable manual capture for UA passports too, and the current test would not catch it. One map instead, same amount of code:
44
-
45
- ```ts
46
- const MANUAL_CAPTURE_DOCS: Record<string, readonly IdType[]> = {
47
- UA: [ID_TYPE.NATIONAL_ID],
48
- };
49
- return (MANUAL_CAPTURE_DOCS[issuingCountry.toUpperCase()] ?? []).includes(idType);
50
- ```
51
-
52
- **8. Doc-type constants live in the wrong file** — `manualCapture.ts:9-13` vs `src/constant/id.ts:4`
53
- Everything else describing "what a document type does" lives in `constant/id.ts` (`idCardTypeMapping`, `ID_TYPE`, `ID_CARD_RATIO`). These new tables sit in `helper/`, so someone adding a doc type will update `constant/id.ts` and never find this file. Suggest keeping `isManualCaptureOnly()` in `helper/` but moving the constant table next to `idCardTypeMapping`.
54
-
55
- **9. `updateType` turns the shutter off just to turn it back on** — `card-camera.ts:552-570`
56
- On a side flip in manual mode we call `disableManualCapture()` then `enableManualCapture()` a few lines later. The round trip only exists to get past the `dataset.manual` guard, and that dependency is invisible unless you read both methods. An early return would let the shutter stay active across the flip:
57
-
58
- ```ts
59
- if (this.manualOnly) {
60
- this.cardType = cardType;
61
- this.hasAutoCaptured = false;
62
- updateTitle();
63
- this.setHint(this.idleHint);
64
- return;
65
- }
66
- ```
67
-
68
- ---
69
-
70
- ### Can wait (follow-up is fine)
71
-
72
- **10.** `src/pageClass/VerifyId.ts:282` — if `defaultCountry` were ever empty, manual mode would silently skip and the upload would send an empty `issuing_country`. Not an issue today since country selection is enforced on the first page, but worth defensive handling if that enforcement is ever removed.
73
-
74
- **11.** `card-camera.ts:797-798` — the `manualOnly` branch in `cancelHold()` only resets detector state that manual mode never sets, so it is dead code once finding 6's getter exists.
75
-
76
- **12.** `VerifyId.ts:277`, `card-camera.ts:688`, `:558` — comments are long and sit in the wrong place. The "init() carries no model work" note is a fact about `card-detection.ts` and belongs on `CardDetection.init` itself (`card-detection.ts:303`).
77
-
78
- **13.** `card-camera.ts:584-594` — the `dataset.manual` early return sits above the "scanner unavailable" dialog, so if anyone ever runs the detector in manual mode the Retry dialog would silently never show. Moving the dialog check up removes the trap.
79
-
80
- **14.** `card-camera.ts:716`, `:531` — the `screen.orientation` listener is never removed, so every `CARDCAMERA` instance leaks after teardown along with its canvases (~7.7MB each). `_cleanup()` also does not detach `_manualClickHandler`. Pre-existing, but this MR's handler is now part of what gets retained.
81
-
82
- **15.** `manualCapture.ts:9` — the country rule is hardcoded, unlike `excludedDocTypes` which comes from the backend token. The next booklet country will need a package release plus every merchant upgrading. Fine for a first version, worth raising with backend later.
83
-
84
- **16.** `card-camera.ts:684`, `VerifyId.ts:282` — `manualOnly` is fixed for the life of the camera and `loadModel()` never runs in manual mode, so a future mid-session mode switch would report "scanner unavailable" on a healthy device. Loading the model inside `startDetection()` would keep it switchable.
85
-
86
- **17.** `src/translations/index.ts:12` — `mapped[key] || ""` means a second locale added without `doc_manual_capture` renders an empty hint rather than falling back to English. No live impact while `en.ts` is the only locale.
87
-
88
- **18.** *(addressed in `e97f4fd`)* `card-camera.ts:696` — shutter activation relies on event order: `#takePicButton` only exists when `loadedmetadata` runs because the video `resize` event (which builds the footer) happens to fire first. Verified that Chromium and WebKit both fire `resize` first, so this works today, but the spec does not guarantee the order. Also calling `enableManualCapture()` at the end of `renderCameraBottomUi()` when `manualOnly` is true (safe, it no-ops if already active) would make activation order-independent.