@globalpayments-private/gpds-pattern-web 0.0.0-stage → 0.1.0-pr.7.gdcb74cd

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.
Files changed (97) hide show
  1. package/README.md +567 -2
  2. package/dist/cjs/control-strategy-DhhYTbse.js +113 -0
  3. package/dist/cjs/gpds-pattern-web.cjs.js +24 -0
  4. package/dist/cjs/index-Dzm6P89U.js +1565 -0
  5. package/dist/cjs/index.cjs.js +180 -0
  6. package/dist/cjs/loader.cjs.js +12 -0
  7. package/dist/cjs/uds-pattern-login-card_2.cjs.entry.js +1662 -0
  8. package/dist/collection/collection-manifest.json +14 -0
  9. package/dist/collection/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.js +63 -0
  10. package/dist/collection/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.js +357 -0
  11. package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.js +100 -0
  12. package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.js +129 -0
  13. package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.js +192 -0
  14. package/dist/collection/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.js +65 -0
  15. package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.css +154 -0
  16. package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.js +508 -0
  17. package/dist/collection/components/uds-pattern-login-card/validation/login-validation-rules.js +86 -0
  18. package/dist/collection/components/uds-pattern-login-card/view-model/login-card-handlers.js +1 -0
  19. package/dist/collection/components/uds-pattern-login-card/view-model/login-step.view-model.js +138 -0
  20. package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.css +37 -0
  21. package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.js +97 -0
  22. package/dist/collection/index.js +5 -0
  23. package/dist/collection/providers/auth-provider.js +28 -0
  24. package/dist/collection/providers/control-strategy.js +21 -0
  25. package/dist/collection/providers/login-config.js +4 -0
  26. package/dist/collection/providers/memory-auth-provider.js +133 -0
  27. package/dist/collection/providers/parse-auth-result.js +135 -0
  28. package/dist/collection/slimmer/common/log.js +14 -0
  29. package/dist/collection/slimmer/common/symbols.js +10 -0
  30. package/dist/collection/slimmer/common/types.js +2 -0
  31. package/dist/collection/slimmer/decorators/inject-slimmer.js +73 -0
  32. package/dist/collection/slimmer/decorators/map-to-component-field.js +41 -0
  33. package/dist/collection/slimmer/decorators/map-to-component-method.js +33 -0
  34. package/dist/collection/slimmer/decorators/slimmer.js +10 -0
  35. package/dist/collection/slimmer/index.js +5 -0
  36. package/dist/collection/utils/labels.js +81 -0
  37. package/dist/collection/utils/types.js +1 -0
  38. package/dist/components/index.d.ts +35 -0
  39. package/dist/components/index.js +1 -0
  40. package/dist/components/p-DOrg55po.js +1 -0
  41. package/dist/components/p-_G8A9gg0.js +1 -0
  42. package/dist/components/uds-pattern-login-card.d.ts +11 -0
  43. package/dist/components/uds-pattern-login-card.js +1 -0
  44. package/dist/components/uds-pattern-login-page.d.ts +11 -0
  45. package/dist/components/uds-pattern-login-page.js +1 -0
  46. package/dist/docs.d.ts +480 -0
  47. package/dist/docs.json +549 -0
  48. package/dist/esm/control-strategy-DOrg55po.js +105 -0
  49. package/dist/esm/gpds-pattern-web.js +20 -0
  50. package/dist/esm/index-DsXlLyGj.js +1537 -0
  51. package/dist/esm/index.js +170 -0
  52. package/dist/esm/loader.js +10 -0
  53. package/dist/esm/uds-pattern-login-card_2.entry.js +1659 -0
  54. package/dist/gpds-pattern-web/gpds-pattern-web.css +1 -0
  55. package/dist/gpds-pattern-web/gpds-pattern-web.esm.js +1 -0
  56. package/dist/gpds-pattern-web/index.esm.js +1 -0
  57. package/dist/gpds-pattern-web/p-2c97d2da.entry.js +1 -0
  58. package/dist/gpds-pattern-web/p-DOrg55po.js +1 -0
  59. package/dist/gpds-pattern-web/p-DsXlLyGj.js +3 -0
  60. package/dist/index.cjs.js +1 -0
  61. package/dist/index.js +1 -0
  62. package/dist/types/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.d.ts +21 -0
  63. package/dist/types/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.d.ts +100 -0
  64. package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.d.ts +44 -0
  65. package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.d.ts +91 -0
  66. package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.d.ts +40 -0
  67. package/dist/types/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.d.ts +21 -0
  68. package/dist/types/components/uds-pattern-login-card/uds-pattern-login-card.d.ts +134 -0
  69. package/dist/types/components/uds-pattern-login-card/validation/login-validation-rules.d.ts +72 -0
  70. package/dist/types/components/uds-pattern-login-card/view-model/login-card-handlers.d.ts +21 -0
  71. package/dist/types/components/uds-pattern-login-card/view-model/login-step.view-model.d.ts +96 -0
  72. package/dist/types/components/uds-pattern-login-page/uds-pattern-login-page.d.ts +21 -0
  73. package/dist/types/components.d.ts +327 -0
  74. package/dist/types/index.d.ts +10 -0
  75. package/dist/types/providers/auth-provider.d.ts +168 -0
  76. package/dist/types/providers/control-strategy.d.ts +144 -0
  77. package/dist/types/providers/login-config.d.ts +52 -0
  78. package/dist/types/providers/memory-auth-provider.d.ts +37 -0
  79. package/dist/types/providers/parse-auth-result.d.ts +8 -0
  80. package/dist/types/slimmer/common/log.d.ts +8 -0
  81. package/dist/types/slimmer/common/symbols.d.ts +10 -0
  82. package/dist/types/slimmer/common/types.d.ts +30 -0
  83. package/dist/types/slimmer/decorators/inject-slimmer.d.ts +24 -0
  84. package/dist/types/slimmer/decorators/map-to-component-field.d.ts +16 -0
  85. package/dist/types/slimmer/decorators/map-to-component-method.d.ts +11 -0
  86. package/dist/types/slimmer/decorators/slimmer.d.ts +10 -0
  87. package/dist/types/slimmer/index.d.ts +6 -0
  88. package/dist/types/stencil-public-runtime.d.ts +1879 -0
  89. package/dist/types/utils/labels.d.ts +13 -0
  90. package/dist/types/utils/types.d.ts +106 -0
  91. package/dist/types/vega.d.ts +11 -0
  92. package/loader/cdn.js +1 -0
  93. package/loader/index.cjs.js +1 -0
  94. package/loader/index.d.ts +24 -0
  95. package/loader/index.es2017.js +1 -0
  96. package/loader/index.js +2 -0
  97. package/package.json +82 -4
package/README.md CHANGED
@@ -1,3 +1,568 @@
1
- # Temporary Holding Version
1
+ # gpds-pattern-web
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ GP pattern library for Web — domain-neutral application patterns composed from
4
+ UDS components. Native parity ships separately as `gpds-pattern-native`.
5
+
6
+ Patterns are authored as [Stencil](https://stenciljs.com) web components so they
7
+ can be consumed from React, Angular, Vue or plain HTML. See
8
+ [ADR 0001](docs/adr/0001-web-component-framework.md) for that decision and
9
+ [ADR 0002](docs/adr/0002-provider-driven-patterns.md) for the API shape.
10
+
11
+ > **Naming.** Custom elements use the `gp-` prefix and CSS custom properties use
12
+ > `--uds-pattern-*`. The Stencil namespace remains `gpds-pattern-web` to match the
13
+ > repository; the package is published as
14
+ > [`@globalpayments-private/gpds-pattern-web`](https://www.npmjs.com/package/@globalpayments-private/gpds-pattern-web).
15
+
16
+ ## Design principles
17
+
18
+ 1. **Patterns own the workflow.** A pattern internalises step sequencing, busy
19
+ state, failure copy, throttling and focus management. Consumers do not
20
+ reimplement those per portal — that duplication is the problem the library
21
+ exists to remove.
22
+ 2. **Consumers own their platform, via a Provider.** Integration is a thin
23
+ adapter implementing a provider contract. The library ships **zero SDK code**
24
+ and never holds credentials or tokens.
25
+ 3. **Capabilities, not configuration.** Which optional provider methods you
26
+ implement determines which routes render. There is no way to show a link the
27
+ backend cannot service.
28
+ 4. **Compose the design system; never re-implement it.** Every control inside a
29
+ pattern is a Vega element. A primitive we copy is a primitive that stops
30
+ receiving the design system's accessibility and contrast fixes — see
31
+ [ADR 0004](docs/adr/0004-compose-design-system-and-renderer-slimmers.md).
32
+ Patterns own *layout*, and style it through the `--uds-pattern-*` custom properties in
33
+ [`src/global/tokens.css`](src/global/tokens.css).
34
+ 5. **Copy is overridable.** Every user-facing string — including error copy —
35
+ is exposed through `config.labels` so patterns localise without forking.
36
+
37
+ ## Live demo
38
+
39
+ A browsable demo is published to GitHub Pages from `main` by
40
+ [`.github/workflows/demo-pages.yml`](.github/workflows/demo-pages.yml).
41
+
42
+ **Nothing is published to npm to make that work.** The workflow builds the
43
+ library from source and serves the compiled bundle next to the page, so looking
44
+ at a component never requires cutting a release. Vega comes from the public
45
+ registry at build time like any other dependency.
46
+
47
+ The demo runs the real compiled component — not a mock — and lets you switch
48
+ between adapters to see the claim that *capabilities are the configuration*:
49
+
50
+ | Scenario | What it shows |
51
+ | --- | --- |
52
+ | Full journey | password → MFA method → one-time code |
53
+ | One MFA method | the picker is skipped, because one option is not a choice |
54
+ | Sign-in only | recovery and sign-up links are *absent*, as the adapter lacks those methods |
55
+ | Lockout after 1 failure | the terminal locked step |
56
+ | Localised (es) | `config.labels` overrides, with Vega still owning field-level validation copy |
57
+ | Control strategy | sign-up intercepted and handed to the host |
58
+
59
+ An event panel shows everything a host application would receive. Validation
60
+ failures never appear there, which is the visible proof that `vega-form` gates
61
+ submission before the adapter is called.
62
+
63
+ The demo shows *behaviour*, not the code a host would write. Beside it,
64
+ **Open as editable source** launches
65
+ [`vega-sandbox`](https://github.com/heartlandpayments/vega-sandbox), which runs
66
+ a real `npm install` and Vite dev server in the browser — the integration code
67
+ on the left, the same card rendering on the right. It needed no fork and no Nx
68
+ migration, only a library that can actually be installed from a browser, which
69
+ is what [publishing](#publishing) unblocked.
70
+
71
+ The project it opens is generated at build time by `npm run build:sandbox` and
72
+ pinned to the exact version that build published, so the source shown is the
73
+ source running. It opens in a new tab rather than an iframe because
74
+ WebContainers need cross-origin isolation, which an iframe can only inherit
75
+ from its top-level page. On `main` the link is replaced by a note until a
76
+ release is cut — the sandbox can only install versions that exist. See
77
+ [ADR 0007](docs/adr/0007-code-and-preview-in-the-demo.md).
78
+
79
+ ### Per-PR previews
80
+
81
+ Every pull request gets its own demo, so a change can be looked at before it
82
+ merges:
83
+
84
+ | URL | What it is |
85
+ | --- | --- |
86
+ | `/` | the latest commit on `main` |
87
+ | `/pr-<number>/` | that pull request, rebuilt on every push |
88
+
89
+ A bot comment on the PR links to its preview, and the preview directory is
90
+ deleted when the PR closes.
91
+
92
+ This works because a repository has **exactly one** Pages site. The official
93
+ `actions/deploy-pages` route replaces that whole site with a single artifact,
94
+ so a preview deploy and a `main` deploy would overwrite each other —
95
+ whichever ran last would win. Instead both publish as *directories* on the
96
+ `gh-pages` branch: a `main` publish deliberately preserves every `pr-*`
97
+ directory, and a preview publish only ever touches its own. Both workflows
98
+ share one concurrency group, because they push to the same branch. See
99
+ [ADR 0006](docs/adr/0006-github-pages-preview-strategy.md).
100
+
101
+ ## Getting started
102
+
103
+ ### Install (consumers)
104
+
105
+ ```bash
106
+ npm install @globalpayments-private/gpds-pattern-web @globalpayments/vega
107
+ ```
108
+
109
+ Vega is a **peer dependency** — a consuming app already has it, and bundling a
110
+ second copy would register the custom elements twice.
111
+
112
+ ### Develop (this repository)
113
+
114
+ ```bash
115
+ npm install
116
+ npm start # dev server with the demo at src/index.html
117
+ npm run build # production build + generated component docs
118
+ npm test # spec tests
119
+ npm run test:coverage # spec tests with the enforced coverage thresholds
120
+ npm run e2e # build, serve the React host, run the Cypress suite against it
121
+ ```
122
+
123
+ The demo signs in with `ada@example.com` / `correct-horse`, then any security
124
+ code of `555555`. Three wrong passwords demonstrates the lockout branch.
125
+
126
+ > **`postinstall` patches `@stencil/core`.** Slimmers need it in order to use
127
+ > field initialisers at all; without it every slimmer throws at construction in
128
+ > a built bundle while the spec suite stays green. See
129
+ > [ADR 0005](docs/adr/0005-patch-stencil-for-slimmers.md).
130
+
131
+ ### Publishing
132
+
133
+ [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml)
134
+ publishes to the public npm registry. Push a tag matching `package.json`:
135
+
136
+ ```bash
137
+ npm version patch # or minor / major / prerelease
138
+ git push --follow-tags
139
+ ```
140
+
141
+ It can also be run from the Actions tab, with a **dry-run** option that builds,
142
+ packs and verifies the tarball without publishing.
143
+
144
+ The workflow refuses to publish when the tag disagrees with `package.json`,
145
+ when that version is already on the registry, or when the packed tarball is
146
+ missing an entry point — and it publishes *the tarball it just inspected*, so
147
+ what is verified is what ships. A version containing a `-` is treated as a
148
+ prerelease and goes to the `next` dist-tag, never `latest`.
149
+
150
+ #### Per-PR preview releases
151
+
152
+ Every push to a pull request publishes the commit under review as a prerelease,
153
+ **before** that PR's demo page is deployed:
154
+
155
+ ```
156
+ 0.1.1-pr.6.g2cf3c93 immutable, pinned to one commit
157
+ pr-6 dist-tag, follows the PR head
158
+ ```
159
+
160
+ Without this the preview would be half-stale: the page is rebuilt from the
161
+ branch, but the sandbox beside it installs from the registry, so it would show
162
+ the last *released* code while the live card above it showed the change under
163
+ review — with nothing on screen admitting the difference. The Pages deploy is
164
+ therefore gated on the publish succeeding, and records what it is demonstrating
165
+ in `demo-meta.json` next to the page.
166
+
167
+ Both are retired when the PR closes: the dist-tag is removed and the versions
168
+ are deprecated. The versions themselves remain — npm versions are immutable and
169
+ cannot be withdrawn after 72 hours.
170
+
171
+ > **Preview builds of unmerged branches are published to a public registry.**
172
+ > That is inherent to the WebContainer sandbox, which can only install what has
173
+ > actually been published. Forks never publish — they have no access to the
174
+ > token.
175
+
176
+ #### Shared publish logic
177
+
178
+ Release and preview differ only in which version and which dist-tag. Everything
179
+ else — install, pack, verify, publish — lives once in
180
+ [`.github/actions/npm-publish`](.github/actions/npm-publish/action.yml), a
181
+ composite action both call, so a guard cannot be fixed in one path and
182
+ forgotten in the other.
183
+
184
+ One of those guards exists because it was missed: npm assigns `latest`
185
+ implicitly when a package has no `latest` tag yet, regardless of the `--tag`
186
+ given, so the first preview publish left `npm install` returning a PR build.
187
+ The action now checks `latest` after publishing a prerelease and moves it back
188
+ to the newest real release, warning when there is not one to move it to.
189
+
190
+ > **Until a release tag is cut, there is no stable version.** `latest`
191
+ > currently resolves to a preview. Cutting `v0.1.0` fixes that and is also
192
+ > what enables the sandbox link on the `main` demo.
193
+
194
+ > **The scope is `@globalpayments-private`, but the package is public.** An npm
195
+ > scope is an organisation and a token grants write to specific organisations;
196
+ > ours is for the `globalpayments-private` org, so that is the scope. Package
197
+ > visibility is independent of the org's name, and `access: public` here is
198
+ > deliberate — an unauthenticated `npm install` inside a WebContainer is the
199
+ > whole point (ADR 0007), and a `restricted` package would silently break it.
200
+ > Publishing needs the `NPM_JS_REGISTRY_ACCESS_TOKEN` repository secret.
201
+
202
+ ## Patterns
203
+
204
+ | Element | Purpose |
205
+ | --- | --- |
206
+ | [`uds-pattern-login-card`](src/components/uds-pattern-login-card/readme.md) | The full login workflow — sign-in, sign-up, MFA, OTP, password reset, username recovery, lockout. |
207
+ | [`uds-pattern-login-page`](src/components/uds-pattern-login-page/readme.md) | Centred, responsive page shell for an unauthenticated route. |
208
+
209
+ Each component's `readme.md` is the **generated API reference** — props,
210
+ events, methods and slots, rewritten by `stencil build --docs` on every build.
211
+ Hand-written guidance lives alongside it so the build cannot destroy it:
212
+
213
+ 📖 **[`uds-pattern-login-card` usage guide](src/components/uds-pattern-login-card/usage.md)**
214
+ — worked adapters for real identity platforms, React / Angular / Vue recipes,
215
+ control-strategy patterns, localisation, theming and a troubleshooting table.
216
+
217
+ ## Integrating `uds-pattern-login-card`
218
+
219
+ The entire integration is one adapter. You do not route steps, toggle spinners
220
+ or write failure messages.
221
+
222
+ ```ts
223
+ import type { UdsAuthProvider } from '@globalpayments-private/gpds-pattern-web';
224
+
225
+ const auth: UdsAuthProvider = {
226
+ // The only required method.
227
+ async signIn({ identifier, password }) {
228
+ const res = await myIdp.authenticate(identifier, password);
229
+
230
+ if (res.challenge === 'PASSWORD') return { status: 'password_required' };
231
+ if (res.challenge === 'MFA') return { status: 'mfa_required', methods: res.factors };
232
+ if (res.error === 'NOT_FOUND') return { status: 'error', code: 'account_not_found' };
233
+
234
+ return { status: 'success', session: { identifier, accessToken: res.token } };
235
+ },
236
+
237
+ // Optional methods are capabilities. Implementing `requestPasswordReset`
238
+ // is what makes "Forgot password?" appear.
239
+ async requestPasswordReset({ identifier }) {
240
+ await myIdp.sendResetLink(identifier);
241
+ return { status: 'reset_link_sent', destination: maskEmail(identifier) };
242
+ },
243
+ };
244
+
245
+ document.querySelector('uds-pattern-login-card').auth = auth;
246
+ ```
247
+
248
+ ```html
249
+ <uds-pattern-login-page logo-src="/logo.svg" logo-alt="Global Payments">
250
+ <uds-pattern-login-card></uds-pattern-login-card>
251
+ </uds-pattern-login-page>
252
+ ```
253
+
254
+ The pattern's `--uds-pattern-*` tokens ship as a **separate global stylesheet**,
255
+ so registering the elements is not on its own enough — import it once, at the
256
+ host app's entry point:
257
+
258
+ ```ts
259
+ import '@globalpayments-private/gpds-pattern-web/tokens.css';
260
+ ```
261
+
262
+ Without it every custom property is undefined, which makes each `var()` invalid
263
+ at computed-value time: the card loses its spacing, surface and typography
264
+ while still rendering and behaving correctly, so nothing errors to point at the
265
+ cause. Override the tokens by redeclaring them on `:root` *after* this import.
266
+
267
+ Listen for the outcome:
268
+
269
+ ```ts
270
+ card.addEventListener('udsLoginSuccess', e => router.go('/dashboard', e.detail.session));
271
+ ```
272
+
273
+ ### Provider capabilities
274
+
275
+ | Implement | Unlocks |
276
+ | --- | --- |
277
+ | `signIn` *(required)* | the workflow itself |
278
+ | `verifyPassword` | a dedicated password step (otherwise `signIn` is re-called with both fields) |
279
+ | `selectMfaMethod` | the MFA method picker (auto-skipped when there is only one method) |
280
+ | `verifyOtp` | security-code entry |
281
+ | `resendOtp` | the throttled "Resend code" affordance |
282
+ | `requestPasswordReset` / `resetPassword` | "Forgot password?" and the new-password step |
283
+ | `requestUsernameReminder` | "Forgot username?" |
284
+ | `signUp` | the "Create one" route |
285
+
286
+ ### Error handling
287
+
288
+ Adapters return a `UdsAuthErrorCode`, never a message. The pattern maps codes to
289
+ approved copy, so raw IdP strings never reach a user. Override any of them:
290
+
291
+ ```html
292
+ <uds-pattern-login-card config='{"labels":{"errors":{"account_not_found":"No encontramos esa cuenta."}}}'></uds-pattern-login-card>
293
+ ```
294
+
295
+ ### Validation
296
+
297
+ Form validation is `vega-form`'s, not the pattern's. Its submit handler awaits
298
+ `valid()` across every field it controls and only emits `vegaFormSubmit` once
299
+ they all pass, so an invalid step never reaches your adapter — and the field at
300
+ fault is the thing that reports it, in the design system's error presentation.
301
+
302
+ Fields declare what they need and Vega derives the rest, including the wording:
303
+
304
+ | Declared | Gives you |
305
+ | --- | --- |
306
+ | `required` | required rule, the asterisk on the label, "This field is required" |
307
+ | `type="email"` | email-format rule and "Please enter the correct email format." |
308
+
309
+ Only two checks cannot be expressed that way, so they live in
310
+ [`validation/login-validation-rules.ts`](src/components/uds-pattern-login-card/validation/login-validation-rules.ts):
311
+
312
+ - **`MatchesFieldRule`** — "confirm new password" must equal "new password". It
313
+ depends on a sibling field, so no single-field rule can state it.
314
+ - **`CompleteCodeRule`** — `required` only asks whether a value is non-empty, so
315
+ a half-typed security code would satisfy it and reach the provider.
316
+
317
+ The card still raises a banner, but only for what a *provider* returns — those
318
+ are workflow outcomes, not field errors, and they have no field to attach to.
319
+
320
+ ## Customisation: the escape-hatch ladder
321
+
322
+ Reach for the lowest level that solves your problem (architecture doc §8):
323
+
324
+ | Level | Mechanism | Use when |
325
+ | --- | --- | --- |
326
+ | 1 | Named slots (`brand`, `footer`) | injecting your own content |
327
+ | 2 | CSS custom properties (`--uds-pattern-*`, plus Vega's own) | re-theming |
328
+ | 3 | `config.auth` — a `UdsAuthProvider` | adapting to your identity platform |
329
+ | 4 | `config.control` — a `UdsLoginControlStrategy` | the course of the workflow genuinely differs |
330
+ | 5 | Composition in a layout container | rearranging the page |
331
+ | 6 | Fork | last resort; please open an issue first |
332
+
333
+ Two things that used to be on this ladder are deliberately **not** here any more
334
+ — see [ADR 0003](docs/adr/0003-aggregate-customisation-seams.md):
335
+
336
+ - **Atomic override props** (`controlled` / `step` / `loading` / `error-message`)
337
+ are gone. Customising behaviour through a handful of independent props means a
338
+ new path only works if you remember to set *all* of them; miss one and you get
339
+ a half-configured workflow that still renders. The replacement is a single
340
+ aggregate seam.
341
+ - **CSS `::part` exposure** is gone. A `::part` rule accepts arbitrary
342
+ declarations, so it can hide the error alert, remove a focus ring or break
343
+ contrast — guardrails the design system is accountable for. Custom properties
344
+ stay, because they constrain the *value* rather than handing over the
345
+ declaration.
346
+
347
+ ### `UdsLoginControlStrategy`
348
+
349
+ One object with an interceptor at every checkpoint the workflow has. Each hook
350
+ receives `next()` — the pattern's own behaviour — so you adapt a decision rather
351
+ than reimplement the machine, and a strategy that only observes cannot
352
+ accidentally change the flow.
353
+
354
+ | Checkpoint | Fires | Default |
355
+ | --- | --- | --- |
356
+ | `submit` | before the provider is consulted | calls the provider method for the step |
357
+ | `transition` | on every provider result | maps the result to a step |
358
+ | `navigate` | on every step change | performs the move |
359
+ | `failure` | on every failure | maps the error code to approved copy |
360
+ | `complete` | on success | emits `udsLoginSuccess` |
361
+
362
+ ```ts
363
+ import { goTo, stay } from '@globalpayments-private/gpds-pattern-web';
364
+
365
+ card.config = {
366
+ auth,
367
+ control: {
368
+ // Hand sign-up off to your own journey.
369
+ navigate: (ctx, next) => {
370
+ if (ctx.to === 'sign-up') {
371
+ router.push('/register');
372
+ return stay();
373
+ }
374
+ return next();
375
+ },
376
+
377
+ // Insert a consent gate ahead of the password reset.
378
+ transition: (ctx, next) => {
379
+ const outcome = next();
380
+ return outcome.type === 'goto' && outcome.step === 'reset-password' && !consented
381
+ ? goTo('check-email')
382
+ : outcome;
383
+ },
384
+ },
385
+ };
386
+ ```
387
+
388
+ Because every checkpoint lives in one object, a new path is expressed — and
389
+ reviewed — as a whole. That is the point: it pushes both us and the consumer to
390
+ reason at the workflow level, which is where a pattern library earns its keep.
391
+
392
+ ## Architecture: slimmers
393
+
394
+ The card's behaviour is split across small single-responsibility classes
395
+ (*slimmers*) that bind to the component's state declaratively, rather than
396
+ living in one large component file:
397
+
398
+ ```
399
+ src/components/uds-pattern-login-card/
400
+ uds-pattern-login-card.tsx props, events, state, render shell
401
+ slimmers/
402
+ login-workflow.slimmer.ts the state machine
403
+ resend-throttle.slimmer.ts the resend window and countdown
404
+ identity-memory.slimmer.ts remembered-identifier persistence
405
+ renderers/
406
+ login-card-renderer.tsx card chrome: heading, banner, form
407
+ login-step-renderer.tsx the body of the current step
408
+ login-field-renderer.tsx the Vega controls themselves
409
+ view-model/
410
+ login-step.view-model.ts what to show, derived in one place
411
+ login-card-handlers.ts what a renderer may ask for
412
+ ```
413
+
414
+ Rendering uses the same mechanism, in `slimmers/renderers/` — the convention
415
+ Vega itself follows. A renderer binds to the component's `viewModel` and
416
+ `handlers` with `@MapToComponentField()` and types them by indexing the
417
+ component class, so it cannot drift out of step with what the component exposes.
418
+ Renderers only read; every mutation goes back through `handlers`.
419
+
420
+ The DI framework behind this is in [`src/slimmer/`](src/slimmer/README.md),
421
+ ported from the legacy Vega design system's *Vega Slimmer* (VD-1102 / VCD-20).
422
+
423
+ It is vendored here only until it can be depended on. The framework has since
424
+ been extracted on the design-system side as `@gpds-web/slimmer`
425
+ (`packages/slimmer` in `gpds-web`), but that package is currently private and
426
+ unpublished, so this repository cannot consume it yet. When it ships,
427
+ `src/slimmer/` should be deleted in favour of the dependency — every import
428
+ already goes through `src/slimmer/index.ts`, so the swap is a dependency change
429
+ rather than a refactor. See
430
+ [ADR 0005](docs/adr/0005-patch-stencil-for-slimmers.md).
431
+
432
+ ## Testing
433
+
434
+ Two suites, with different jobs.
435
+
436
+ ```bash
437
+ npm test # spec tests
438
+ npm run test:coverage # the same suite, with coverage thresholds enforced
439
+ npm run e2e # build + serve the React host + drive it with Cypress
440
+ ```
441
+
442
+ ### Spec tests
443
+
444
+ Workflow behaviour is tested through `createMemoryAuthProvider()`, a reference
445
+ fake IdP that is also the clearest worked example of an adapter.
446
+
447
+ Spec tests assert the *contract*, not our markup — which Vega elements we
448
+ render, what we configure them with, and what we do when they report a change.
449
+ Vega's real classes are deliberately **not** registered in `newSpecPage`: that
450
+ would pull the design system's dependency graph into our unit tests and break
451
+ them whenever Vega changed something internal.
452
+
453
+ #### Coverage
454
+
455
+ `npm run test:coverage` fails the build below the thresholds in
456
+ `stencil.config.ts`. The bar is **100% of statements, branches, functions and
457
+ lines, globally, with no per-file exceptions.**
458
+
459
+ That is deliberate. A per-file relaxation hides the interesting question —
460
+ *why* is this branch unreachable? — behind a number. Every exception this
461
+ suite used to carry turned out to be either dead code or a missing test, and
462
+ none of them survived the question:
463
+
464
+ | Was excused | What it actually was |
465
+ | --- | --- |
466
+ | `focusFirstControl`'s `?.` chains | Defensive code for a missing shadow root on a `shadow: true` component, and a missing `.card__heading` that every step renders. Both deleted, with a comment recording why they cannot happen. |
467
+ | `result.methods ?? []` (×2) | `UdsAuthResult` declares `methods` as **required** on the `mfa_required` arm and `parseAuthResult` asserts it. Dead code. Deleted. |
468
+ | `textField`'s `required = true` default | Unused configuration — no caller ever overrode it. The option was removed. The same probe found `autocomplete = 'off'` in the same state; since autofill is the difference between a password manager working and silently not, it was made **required** instead of defaulted. |
469
+ | `login-step-renderer`'s `default:` arm | Genuinely unreachable *through the card* — terminal steps are routed elsewhere — but reachable by binding the slimmer to a plain host directly. Now covered by a test. |
470
+ | `errors[code] ?? errors.unknown` | Impossible by type, but reachable in practice: a server-driven label override expresses a missing translation as `null`, which spreads a hole into the merged map. Kept, and now covered by a test. |
471
+
472
+ No `istanbul ignore` comments were needed. If one ever is, it belongs on the
473
+ single line with a comment explaining the impossibility — not on a whole file.
474
+
475
+ ### End-to-end tests
476
+
477
+ The spec boundary — everything that depends on Vega's real classes — has to be
478
+ checked in a real browser. Doing so has caught five bugs that every spec passed
479
+ through: `vega-button[type="submit"]` being unable to submit a native ancestor
480
+ `<form>` across its shadow boundary; loose `vega-selection-tile` elements
481
+ throwing on unmount without their group ancestor; and a slimmer without an
482
+ explicit constructor rendering an entirely **empty component** while all 74
483
+ specs stayed green (now fixed at source — ADR 0005).
484
+
485
+ The Cypress suite in [`cypress/`](cypress) closes that gap, and it drives a
486
+ **React application** — [`e2e-app/`](e2e-app) — rather than a static page.
487
+
488
+ That is the point. The static demo proves the component works; a React host
489
+ proves the component is *integrable*, and React 18 is where custom elements
490
+ actually bite:
491
+
492
+ - **Object props stringify.** React 18 sets any prop it does not recognise as
493
+ an attribute, so `config={adapter}` arrives as the string
494
+ `"[object Object]"`. The config has to be assigned to the element *property*,
495
+ from a `ref`.
496
+ - **Custom events have no JSX surface.** `onUdsLoginSuccess` is not a thing;
497
+ `udsLoginStepChange` and friends need `addEventListener`.
498
+
499
+ Both live in [`e2e-app/src/useLoginCard.ts`](e2e-app/src/useLoginCard.ts), which
500
+ is as close to documentation of the React integration as it is test
501
+ scaffolding. Standing it up immediately found two bugs the static demo could
502
+ never have: the package's `exports` map pointed `import` at
503
+ `dist/esm/index.mjs`, a file the build does not emit — so **every ESM consumer
504
+ importing the package by name failed to resolve it** — and a listener effect
505
+ keyed on a ref rather than the element went deaf after a remount.
506
+
507
+ The app consumes the **built package**, not `src/`, through a `file:..`
508
+ dependency that npm installs as a symlink. There is no bundler alias, so what
509
+ Cypress drives is exactly what a consumer would install, `exports` map included
510
+ — which is why the broken entry point surfaced at all.
511
+
512
+ The static demo at `src/index.html` is untouched and still what GitHub Pages
513
+ publishes; it is now a demo rather than a test fixture.
514
+
515
+ > Vite + React rather than Next.js: SSR has no bearing on what this suite
516
+ > checks, and custom elements under Next would need `ssr: false` dynamic
517
+ > imports throughout — ceremony that tests the workaround, not the component.
518
+ > Say the word if a Next host is wanted and it can be swapped in.
519
+
520
+ ```bash
521
+ npm run e2e # one shot: build package, build app, serve, run, stop
522
+ # or, against a server you are already running:
523
+ npm run e2e-app:build
524
+ npm run serve:e2e-app &
525
+ npm run cypress:run # headless
526
+ npm run cypress:open # interactive
527
+ ```
528
+
529
+ What it covers: the full journey (identifier → password → MFA choice → code →
530
+ signed in), capability-driven rendering, the lockout branch, localisation,
531
+ control-strategy interception, and validation gating — each invalid submit
532
+ asserted to reach the adapter **zero** times, read off the host's event log.
533
+
534
+ Two things to know before writing more specs:
535
+
536
+ - **`includeShadowDom: true`** is set in `cypress.config.ts` and is not
537
+ optional: everything this library renders lives inside a shadow root.
538
+ - **Text inputs are typed into with `{ force: true }`.** Cypress 13 misreads an
539
+ `<input>` reached through two shadow roots as non-focusable, although the
540
+ same element passes `:not([disabled])`, `be.visible` and `.click()`. The
541
+ keystrokes are real; only the pre-flight check is wrong. `cy.fillField()`
542
+ already handles this.
543
+ - **`vega-input-passcode` is driven through its `vegaChange` event**, not
544
+ keystrokes. Typing into its boxes — or dispatching a bare `input` event at
545
+ them — hangs the renderer indefinitely. That reproduces on a standalone,
546
+ *uncontrolled* `vega-input-passcode` on an otherwise empty page, so it is a
547
+ Vega-side interaction with synthetic events rather than anything this pattern
548
+ introduces. `cy.fillPasscode()` wraps the workaround.
549
+
550
+ #### Linux prerequisites
551
+
552
+ Cypress needs an X server and a few shared libraries that a bare container does
553
+ not have:
554
+
555
+ ```bash
556
+ sudo apt-get install -y xvfb libgtk-3-0 libgbm1 libnotify4 libnss3 libxss1 libasound2
557
+ ```
558
+
559
+ #### CI
560
+
561
+ **Not wired up yet, deliberately.** `gpds-pattern-web` has two registered
562
+ workflows (`demo-pages.yml`, `demo-preview-cleanup.yml`), both `active`, and
563
+ **neither has ever produced a run** — including on the push to `main` that
564
+ merged them. `gpds-web` has no workflows at all and no in-repo pipeline
565
+ definition, so there is no existing Cypress CI setup to mirror. Both suites are
566
+ single commands (`npm run test:coverage`, `npm run e2e`) and are ready to
567
+ be dropped into whichever system actually runs; adding a config that silently
568
+ never executes would be worse than leaving this blank.
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Default (en-US) copy for the login workflow pattern.
5
+ *
6
+ * Wording mirrors the approved Global Payments sign-in design
7
+ * (Figma: "Design System and Pilot Portals Rollout", node 14:2748 / 14:2765).
8
+ *
9
+ * The `errors` map is part of the pattern, not the consumer: shipping approved
10
+ * failure copy is how every GP product ends up failing in the same words.
11
+ */
12
+ const DEFAULT_LOGIN_LABELS = {
13
+ identifierHeading: 'Sign In to My Account',
14
+ identifierFieldLabel: 'Email address',
15
+ rememberIdentifier: 'Remember my email',
16
+ passwordHeading: 'Enter your password',
17
+ passwordFieldLabel: 'Password',
18
+ showPassword: 'Show password',
19
+ hidePassword: 'Hide password',
20
+ forgotPassword: 'Forgot password?',
21
+ forgotUsername: 'Forgot username?',
22
+ mfaMethodHeading: 'How do you want to receive your security code?',
23
+ mfaMethodDescription: 'Choose a delivery method to continue signing in.',
24
+ otpHeading: 'Verify your security code',
25
+ otpDescription: 'Enter the security code generated from your authenticator app.',
26
+ otpDescriptionForMethod: 'Enter the security code sent via {method}.',
27
+ otpFieldLabel: 'Security code',
28
+ otpIncomplete: 'Enter all {count} digits of your security code.',
29
+ otpSubmit: 'Verify and sign in',
30
+ showCode: 'Show security code',
31
+ hideCode: 'Hide security code',
32
+ resendCode: 'Resend code',
33
+ resendCodeIn: 'Resend code in {seconds}s',
34
+ changeMfaMethodPrompt: 'Want to change how you receive your security code?',
35
+ changeMfaMethod: 'Change security method',
36
+ forgotPasswordHeading: 'Reset your password',
37
+ forgotPasswordDescription: 'Enter your email address and we will send you a link to reset your password.',
38
+ forgotUsernameHeading: 'Find your username',
39
+ forgotUsernameDescription: 'Enter the email address associated with your account.',
40
+ checkEmailHeading: 'Check your email',
41
+ checkEmailDescription: 'If an account matches what you entered, we have sent you an email with next steps.',
42
+ checkEmailDescriptionWithDestination: 'If an account matches {destination}, we have sent an email with next steps.',
43
+ lockedHeading: 'Your account is temporarily locked',
44
+ lockedDescription: 'For your security we have paused sign-in after too many attempts. Try again later.',
45
+ lockedDescriptionWithTime: 'For your security we have paused sign-in after too many attempts. Try again in {minutes} minutes.',
46
+ resetPasswordHeading: 'Create a new password',
47
+ newPasswordFieldLabel: 'New password',
48
+ confirmPasswordFieldLabel: 'Confirm new password',
49
+ signUpHeading: 'Create your account',
50
+ signUpDescription: 'Tell us a little about you to get started.',
51
+ firstNameFieldLabel: 'First name',
52
+ lastNameFieldLabel: 'Last name',
53
+ signUpSubmit: 'Create account',
54
+ haveAccountPrompt: 'Already have an account?',
55
+ signIn: 'Sign in',
56
+ successHeading: "You're signed in",
57
+ continue: 'Continue',
58
+ back: 'Back',
59
+ troubleSigningInPrompt: 'Forgot password? Account locked? Change security method?',
60
+ troubleSigningIn: 'Trouble Signing In?',
61
+ helpPrompt: 'Need help signing in? Check out our',
62
+ help: 'Login help',
63
+ noAccountPrompt: "Don't have an account?",
64
+ createAccount: 'Create one',
65
+ errors: {
66
+ invalid_credentials: "That email address and password don't match. Try again.",
67
+ invalid_identifier: 'Enter a valid email address.',
68
+ invalid_code: "That security code isn't right. Check it and try again.",
69
+ code_expired: 'That security code has expired. Request a new one.',
70
+ account_locked: 'Your account is temporarily locked. Try again later.',
71
+ account_not_found: "We couldn't find an account for that email address.",
72
+ account_exists: 'An account already exists for that email address.',
73
+ password_policy: "That password doesn't meet the security requirements.",
74
+ passwords_do_not_match: "Those passwords don't match.",
75
+ rate_limited: 'Too many attempts. Wait a moment and try again.',
76
+ network: "We couldn't reach the server. Check your connection and try again.",
77
+ unknown: 'Something went wrong. Try again.',
78
+ },
79
+ };
80
+ /** Substitutes `{name}` placeholders in a label template. */
81
+ function formatLabel(template, values) {
82
+ return template.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
83
+ }
84
+
85
+ /** Moves the workflow to `step`. */
86
+ const goTo = (step) => ({ type: 'goto', step });
87
+ /** Leaves the workflow where it is. */
88
+ const stay = () => ({ type: 'stay' });
89
+ /** Surfaces a failure without moving. */
90
+ const fail = (code, message) => ({
91
+ type: 'fail',
92
+ code,
93
+ message,
94
+ });
95
+ /**
96
+ * Runs a checkpoint through the strategy, falling back to the pattern's own
97
+ * behaviour when the strategy does not intercept it.
98
+ */
99
+ function intercept(interceptor, context, fallback) {
100
+ return interceptor ? interceptor(context, fallback) : fallback();
101
+ }
102
+ /** Identity helper giving a strategy literal its types without a type annotation. */
103
+ function createControlStrategy(strategy) {
104
+ return strategy;
105
+ }
106
+
107
+ exports.DEFAULT_LOGIN_LABELS = DEFAULT_LOGIN_LABELS;
108
+ exports.createControlStrategy = createControlStrategy;
109
+ exports.fail = fail;
110
+ exports.formatLabel = formatLabel;
111
+ exports.goTo = goTo;
112
+ exports.intercept = intercept;
113
+ exports.stay = stay;