@clicktap/ui 2.3.3 → 2.3.4
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.
- package/components/PasswordCheck/PasswordCheck.js +1 -1
- package/components/PasswordCheck/PasswordCheck.utils.d.ts +15 -0
- package/components/PasswordCheck/PasswordCheck.utils.js +1 -1
- package/docs/migrations/2026/09/ui-2026-09-24-password-check-lowercase-and-trim.md +81 -0
- package/docs/migrations/index.json +10 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsxs as n,jsx as t}from"react/jsx-runtime";import{cn as
|
|
1
|
+
"use client";import{jsxs as n,jsx as t}from"react/jsx-runtime";import{cn as r}from"../../utils/cn.js";import{trimEdgeWhitespace as g,checkStrength as x,getProgressText as o}from"./PasswordCheck.utils.js";const f=[{label:"At least 8 characters",test:s=>s.length>=8},{label:"At least one uppercase letter",test:s=>/[A-Z]/.test(s)},{label:"At least one lowercase letter",test:s=>/[a-z]/.test(s)},{label:"At least one digit",test:s=>/\d/.test(s)},{label:"At least one special character",test:s=>/[^A-Za-z0-9]/.test(s)}];function p({value:s,variant:i="default",requirements:d=f}){const c=g(s),e=x(c);return i==="requirements"?s?n("div",{className:"mt-4 flex flex-col gap-2",children:[t("ul",{className:"flex flex-col gap-1 text-xs text-left",children:d.map(a=>{const l=a.test(c);return n("li",{className:"flex items-center gap-1.5",children:[t("svg",{xmlns:"http://www.w3.org/2000/svg",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",className:r("shrink-0 transition-colors",l?"text-success":"text-fg-subtle"),children:l?t("path",{d:"M20 6 9 17l-5-5"}):t("circle",{cx:"12",cy:"12",r:"4"})}),t("span",{className:r("transition-colors",l?"text-success":"text-fg-muted"),children:a.label})]},a.label)})}),n("div",{className:"flex flex-col",children:[t("div",{className:"w-full flex gap-x-2",children:Array.from({length:5},(a,l)=>l).map((a,l)=>t("div",{className:r("grow shrink basis-1/5 h-1 rounded-full transition-colors duration-700 ease bg-border-subtle",e<=l&&"bg-fg-muted",(e===1||e===2)&&"bg-danger",e===3&&"bg-warning",e===4&&"bg-success",e===5&&"bg-info")},a))}),n("div",{className:r("inline-flex gap-x-1 text-xs text-fg-strong transition-opacity mt-1",e?"opacity-100":"opacity-0"),children:[t("span",{children:"Password Strength:"}),t("span",{className:r((e===1||e===2)&&"text-danger",e===3&&"text-warning",e===4&&"text-success",e===5&&"text-info"),children:o(e)})]})]})]}):null:t("div",{children:n("div",{className:r("relative w-full flex gap-x-2",i==="default"&&"mb-8 -mt-1"),children:[Array.from({length:5},(a,l)=>l).map((a,l)=>t("div",{className:r("grow shrink basis-1/5 h-1 rounded-full transition-colors duration-700 ease bg-border-subtle",e<=l&&"bg-fg-muted",(e===1||e===2)&&"bg-danger",e===3&&"bg-warning",e===4&&"bg-success",e===5&&"bg-info")},a)),i==="default"&&n("div",{className:r("inline-flex gap-x-1 absolute top-0 left-0 text-xs translate-y-2/4 text-fg-strong",e?"opacity-100":"opacity-0"),children:[t("span",{children:"Password Strength:"}),t("span",{className:r((e===1||e===2)&&"text-danger",e===3&&"text-warning",e===4&&"text-success",e===5&&"text-info"),children:o(e)})]})]})})}export{p as PasswordCheck,p as default};
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip edge whitespace the way the backend does before it validates a password.
|
|
3
|
+
*
|
|
4
|
+
* The policy lives in PHP — `PasswordService::getPasswordSchema()` in
|
|
5
|
+
* `libs/mezzio-modules/user/src/Service/PasswordService.php` — and the schema opens with
|
|
6
|
+
* `->trim()`, which is `Rmg\Validator\EdgeWhitespace`, not PHP's `trim()`. Every rule after it,
|
|
7
|
+
* `min(8)` included, sees the trimmed value. Measuring the raw one here told a visitor that
|
|
8
|
+
* `" abcdef1A! "` was 13 characters long while the server counted 9.
|
|
9
|
+
*
|
|
10
|
+
* The character class mirrors that PHP constant: JavaScript's `\s` already covers NBSP, the whole
|
|
11
|
+
* `Zs` category and U+FEFF, so only NUL and U+200B ZERO WIDTH SPACE have to be named (U+FEFF is
|
|
12
|
+
* listed anyway, to keep the two classes readable side by side). Interior whitespace is part of
|
|
13
|
+
* the value and is left alone, on both sides.
|
|
14
|
+
*/
|
|
15
|
+
export declare const trimEdgeWhitespace: (value: string) => string;
|
|
1
16
|
/** @todo these functions should probably be passed as props to be able to extend */
|
|
2
17
|
export declare const checkStrength: (value: string) => number;
|
|
3
18
|
export declare const getProgressText: (strength: number) => "" | "Very Strong" | "Strong" | "Good" | "Weak" | "Very Weak";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const s=t=>{const r=t.length>=8;let e=0;return/^(?=.*[a-z])(?=.*[A-Z])/.test(t)&&r&&(e+=1),/\d/.test(t)&&r&&(e+=1),t.length>=8&&(e+=1),t.length>=11&&(e+=1),/[ `!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/.test(t)&&r&&(e+=1),e},
|
|
1
|
+
const s=t=>t.replace(/^[\s\u0000\u200B\uFEFF]+|[\s\u0000\u200B\uFEFF]+$/g,""),n=t=>{const r=t.length>=8;let e=0;return/^(?=.*[a-z])(?=.*[A-Z])/.test(t)&&r&&(e+=1),/\d/.test(t)&&r&&(e+=1),t.length>=8&&(e+=1),t.length>=11&&(e+=1),/[ `!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/.test(t)&&r&&(e+=1),e},c=t=>{switch(t){case 5:return"Very Strong";case 4:return"Strong";case 3:return"Good";case 2:return"Weak";case 1:return"Very Weak";default:return""}},g=(t="")=>({eightСharacters:t.length>=8,upLowCaseLetters:/^(?=.*[a-z])(?=.*[A-Z])/.test(t),oneNumber:/\d/.test(t)});export{n as checkStrength,g as getPasswordRequirements,c as getProgressText,s as trimEdgeWhitespace};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# BUGFIX: `PasswordCheck` states the password policy the backend actually enforces
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-24
|
|
4
|
+
**Package:** `@clicktap/ui`
|
|
5
|
+
**Affects:** `src/components/PasswordCheck/PasswordCheck.tsx`, `PasswordCheck.utils.ts` — every `<PasswordCheck variant="requirements">` that relies on the default `requirements`.
|
|
6
|
+
**Classification:** BUGFIX
|
|
7
|
+
**Breaking:** No. No API change: `requirements`, `value` and `variant` are untouched, and a consumer passing its own `requirements` is unaffected. **Displayed:** the default list grows from four rules to five, and all five are now evaluated against the edge-trimmed value.
|
|
8
|
+
**Ticket:** CTAP-1906
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`PasswordCheck` restated the password policy in its own words, and the words were wrong.
|
|
13
|
+
|
|
14
|
+
The policy lives in PHP — `PasswordService::getPasswordSchema()`,
|
|
15
|
+
`libs/mezzio-modules/user/src/Service/PasswordService.php`:
|
|
16
|
+
|
|
17
|
+
```php
|
|
18
|
+
$sb->string()->trim()->min(8)
|
|
19
|
+
->withValidator(new AtLeastOneUppercaseValidator()) // /[A-Z]/
|
|
20
|
+
->withValidator(new AtLeastOneLowercaseValidator()) // /[a-z]/
|
|
21
|
+
->withValidator(new AtLeastOneNumberValidator()) // /\d/
|
|
22
|
+
->withValidator(new AtLeastOneSpecialCharacterValidator()); // /[\W_]/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Five rules. The component displayed four — the lowercase rule was missing. So `PASSWORD1!` turned
|
|
26
|
+
**every displayed tick green** and the server refused the save, and because the rule that rejected
|
|
27
|
+
the visitor was never on screen, the form could not tell them what to change.
|
|
28
|
+
|
|
29
|
+
Two narrower mismatches came with it:
|
|
30
|
+
|
|
31
|
+
- **Length was measured on the raw value** while the schema measures the trimmed one. `" abcdef1A! "`
|
|
32
|
+
showed as 13 characters and was validated as 9. The schema's `->trim()` is
|
|
33
|
+
`Rmg\Validator\EdgeWhitespace`, which is wider than PHP's `trim()`: NUL, U+200B and U+FEFF too.
|
|
34
|
+
- **The special-character rule was narrower than the validator.** The component tested a fixed
|
|
35
|
+
punctuation list; `AtLeastOneSpecialCharacterValidator` is `/[\W_]/` — anything outside
|
|
36
|
+
`[A-Za-z0-9]`. A password whose only special character was `£`, a non-ASCII letter or an interior
|
|
37
|
+
space was refused a tick the server would have granted.
|
|
38
|
+
|
|
39
|
+
## What changed
|
|
40
|
+
|
|
41
|
+
`defaultRequirements` is now the five rules above, in schema order, each one carrying a comment
|
|
42
|
+
naming the validator it mirrors:
|
|
43
|
+
|
|
44
|
+
| # | Displayed rule | Backend rule | Before |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| 1 | At least 8 characters | `->min(8)` (post-trim) | measured untrimmed |
|
|
47
|
+
| 2 | At least one uppercase letter | `AtLeastOneUppercaseValidator` | unchanged |
|
|
48
|
+
| 3 | At least one lowercase letter | `AtLeastOneLowercaseValidator` | **missing** |
|
|
49
|
+
| 4 | At least one digit | `AtLeastOneNumberValidator` | unchanged |
|
|
50
|
+
| 5 | At least one special character | `AtLeastOneSpecialCharacterValidator` (`/[\W_]/`) | fixed punctuation list |
|
|
51
|
+
|
|
52
|
+
The component trims once and hands the trimmed value to every rule and to `checkStrength`, because
|
|
53
|
+
the schema's `->trim()` runs before `->min(8)` and before every validator. The new
|
|
54
|
+
`trimEdgeWhitespace` export in `PasswordCheck.utils.ts` mirrors `Rmg\Validator\EdgeWhitespace`:
|
|
55
|
+
JavaScript's `\s` (which already covers NBSP, the `Zs` category and U+FEFF) plus NUL and U+200B.
|
|
56
|
+
Interior whitespace is untouched, on both sides.
|
|
57
|
+
|
|
58
|
+
`PasswordCheck.requirements.test.tsx` pins the rendered rule set and names
|
|
59
|
+
`PasswordService::getPasswordSchema()` as the source of truth, so the next change to the policy on
|
|
60
|
+
either side lands as a failing test pointing at the other half.
|
|
61
|
+
|
|
62
|
+
This is the hand-mirroring fix. Generating the list from the serialized schema, so the two cannot
|
|
63
|
+
drift again, is **CTAP-1908**.
|
|
64
|
+
|
|
65
|
+
## Consumer Impact
|
|
66
|
+
|
|
67
|
+
**No action needed.** No prop, export or type changed, and the extra rule appears on its own.
|
|
68
|
+
|
|
69
|
+
**A consumer that passes its own `requirements` keeps exactly what it passes** — the prop still
|
|
70
|
+
overrides the default outright. But that list has the same hole this fix closed, so check it against
|
|
71
|
+
the table above. One such copy existed in the storefront and was fixed in the same change:
|
|
72
|
+
`apps/frontend/components/Account/Auth/Authenticate/constanst.ts` (`passwordValidation`, the zod gate
|
|
73
|
+
behind signup, reset-password and update-password) was missing the lowercase rule and carried the
|
|
74
|
+
same narrow special-character list.
|
|
75
|
+
|
|
76
|
+
**Rules now receive the trimmed value.** If you pass custom `requirements`, your `test` callbacks
|
|
77
|
+
are called with the edge-trimmed string, not the raw one. A rule that deliberately inspected
|
|
78
|
+
leading/trailing whitespace would stop seeing it — nothing in this repo did.
|
|
79
|
+
|
|
80
|
+
**Visually, the block is one line taller** in the `requirements` variant. Nothing else about its
|
|
81
|
+
layout, the strength meter or the hide-until-typed behavior moved.
|
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
"breaking": "No API change, and no prop, export or class-name contract moves. **Visual:** 76 of the 118 swaps resolve to the exact same light-theme value and render identically. The other 42 do not — they are listed component by component under \"Rendering changes\" and are all one step along a neutral ramp, except `UploadImage`, whose hover background **appears for the first time** because the class it had was a typo.",
|
|
10
10
|
"affects": "54 files under `src/components/**` — every component listed in \"What changed\" below — plus the new root-level `eslint.rules.no-literal-palette-color.mjs` and the two eslint configs that reference it."
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"path": "2026/09/ui-2026-09-24-password-check-lowercase-and-trim.md",
|
|
14
|
+
"module": "libs/frontend-modules/ui",
|
|
15
|
+
"package": "@clicktap/ui",
|
|
16
|
+
"date": "2026-09-24",
|
|
17
|
+
"title": "BUGFIX: `PasswordCheck` states the password policy the backend actually enforces",
|
|
18
|
+
"classification": "BUGFIX",
|
|
19
|
+
"breaking": "No. No API change: `requirements`, `value` and `variant` are untouched, and a consumer passing its own `requirements` is unaffected. **Displayed:** the default list grows from four rules to five, and all five are now evaluated against the edge-trimmed value.",
|
|
20
|
+
"affects": "`src/components/PasswordCheck/PasswordCheck.tsx`, `PasswordCheck.utils.ts` — every `<PasswordCheck variant=\"requirements\">` that relies on the default `requirements`."
|
|
21
|
+
},
|
|
12
22
|
{
|
|
13
23
|
"path": "2026/09/ui-2026-09-24-dialog-semantic-surface-tokens.md",
|
|
14
24
|
"module": "libs/frontend-modules/ui",
|