@globalpayments-private/gpds-pattern-web 0.0.0-stage → 0.1.0-pr.6.g07fbefe

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 +549 -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 +79 -4
package/README.md CHANGED
@@ -1,3 +1,550 @@
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. Putting an
64
+ editable copy of that code beside it — with
65
+ [`vega-sandbox`](https://github.com/heartlandpayments/vega-sandbox), which runs
66
+ a real `npm install` and dev server in the page — needs no fork and no Nx
67
+ migration, only a library that can actually be installed from a browser. That
68
+ is what [publishing](#publishing) unblocked; the wiring itself is still to
69
+ come. See [ADR 0007](docs/adr/0007-code-and-preview-in-the-demo.md).
70
+
71
+ ### Per-PR previews
72
+
73
+ Every pull request gets its own demo, so a change can be looked at before it
74
+ merges:
75
+
76
+ | URL | What it is |
77
+ | --- | --- |
78
+ | `/` | the latest commit on `main` |
79
+ | `/pr-<number>/` | that pull request, rebuilt on every push |
80
+
81
+ A bot comment on the PR links to its preview, and the preview directory is
82
+ deleted when the PR closes.
83
+
84
+ This works because a repository has **exactly one** Pages site. The official
85
+ `actions/deploy-pages` route replaces that whole site with a single artifact,
86
+ so a preview deploy and a `main` deploy would overwrite each other —
87
+ whichever ran last would win. Instead both publish as *directories* on the
88
+ `gh-pages` branch: a `main` publish deliberately preserves every `pr-*`
89
+ directory, and a preview publish only ever touches its own. Both workflows
90
+ share one concurrency group, because they push to the same branch. See
91
+ [ADR 0006](docs/adr/0006-github-pages-preview-strategy.md).
92
+
93
+ ## Getting started
94
+
95
+ ### Install (consumers)
96
+
97
+ ```bash
98
+ npm install @globalpayments-private/gpds-pattern-web @globalpayments/vega
99
+ ```
100
+
101
+ Vega is a **peer dependency** — a consuming app already has it, and bundling a
102
+ second copy would register the custom elements twice.
103
+
104
+ ### Develop (this repository)
105
+
106
+ ```bash
107
+ npm install
108
+ npm start # dev server with the demo at src/index.html
109
+ npm run build # production build + generated component docs
110
+ npm test # spec tests
111
+ npm run test:coverage # spec tests with the enforced coverage thresholds
112
+ npm run e2e # build, serve the React host, run the Cypress suite against it
113
+ ```
114
+
115
+ The demo signs in with `ada@example.com` / `correct-horse`, then any security
116
+ code of `555555`. Three wrong passwords demonstrates the lockout branch.
117
+
118
+ > **`postinstall` patches `@stencil/core`.** Slimmers need it in order to use
119
+ > field initialisers at all; without it every slimmer throws at construction in
120
+ > a built bundle while the spec suite stays green. See
121
+ > [ADR 0005](docs/adr/0005-patch-stencil-for-slimmers.md).
122
+
123
+ ### Publishing
124
+
125
+ [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml)
126
+ publishes to the public npm registry. Push a tag matching `package.json`:
127
+
128
+ ```bash
129
+ npm version patch # or minor / major / prerelease
130
+ git push --follow-tags
131
+ ```
132
+
133
+ It can also be run from the Actions tab, with a **dry-run** option that builds,
134
+ packs and verifies the tarball without publishing.
135
+
136
+ The workflow refuses to publish when the tag disagrees with `package.json`,
137
+ when that version is already on the registry, or when the packed tarball is
138
+ missing an entry point — and it publishes *the tarball it just inspected*, so
139
+ what is verified is what ships. A version containing a `-` is treated as a
140
+ prerelease and goes to the `next` dist-tag, never `latest`.
141
+
142
+ #### Per-PR preview releases
143
+
144
+ Every push to a pull request publishes the commit under review as a prerelease,
145
+ **before** that PR's demo page is deployed:
146
+
147
+ ```
148
+ 0.1.1-pr.6.g2cf3c93 immutable, pinned to one commit
149
+ pr-6 dist-tag, follows the PR head
150
+ ```
151
+
152
+ Without this the preview would be half-stale: the page is rebuilt from the
153
+ branch, but the sandbox beside it installs from the registry, so it would show
154
+ the last *released* code while the live card above it showed the change under
155
+ review — with nothing on screen admitting the difference. The Pages deploy is
156
+ therefore gated on the publish succeeding, and records what it is demonstrating
157
+ in `demo-meta.json` next to the page.
158
+
159
+ Both are retired when the PR closes: the dist-tag is removed and the versions
160
+ are deprecated. The versions themselves remain — npm versions are immutable and
161
+ cannot be withdrawn after 72 hours.
162
+
163
+ > **Preview builds of unmerged branches are published to a public registry.**
164
+ > That is inherent to the WebContainer sandbox, which can only install what has
165
+ > actually been published. Forks never publish — they have no access to the
166
+ > token.
167
+
168
+ #### Shared publish logic
169
+
170
+ Release and preview differ only in which version and which dist-tag. Everything
171
+ else — install, pack, verify, publish — lives once in
172
+ [`.github/actions/npm-publish`](.github/actions/npm-publish/action.yml), a
173
+ composite action both call, so a guard cannot be fixed in one path and
174
+ forgotten in the other.
175
+
176
+ > **The scope is `@globalpayments-private`, but the package is public.** An npm
177
+ > scope is an organisation and a token grants write to specific organisations;
178
+ > ours is for the `globalpayments-private` org, so that is the scope. Package
179
+ > visibility is independent of the org's name, and `access: public` here is
180
+ > deliberate — an unauthenticated `npm install` inside a WebContainer is the
181
+ > whole point (ADR 0007), and a `restricted` package would silently break it.
182
+ > Publishing needs the `NPM_JS_REGISTRY_ACCESS_TOKEN` repository secret.
183
+
184
+ ## Patterns
185
+
186
+ | Element | Purpose |
187
+ | --- | --- |
188
+ | [`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. |
189
+ | [`uds-pattern-login-page`](src/components/uds-pattern-login-page/readme.md) | Centred, responsive page shell for an unauthenticated route. |
190
+
191
+ Each component's `readme.md` is the **generated API reference** — props,
192
+ events, methods and slots, rewritten by `stencil build --docs` on every build.
193
+ Hand-written guidance lives alongside it so the build cannot destroy it:
194
+
195
+ 📖 **[`uds-pattern-login-card` usage guide](src/components/uds-pattern-login-card/usage.md)**
196
+ — worked adapters for real identity platforms, React / Angular / Vue recipes,
197
+ control-strategy patterns, localisation, theming and a troubleshooting table.
198
+
199
+ ## Integrating `uds-pattern-login-card`
200
+
201
+ The entire integration is one adapter. You do not route steps, toggle spinners
202
+ or write failure messages.
203
+
204
+ ```ts
205
+ import type { UdsAuthProvider } from '@globalpayments-private/gpds-pattern-web';
206
+
207
+ const auth: UdsAuthProvider = {
208
+ // The only required method.
209
+ async signIn({ identifier, password }) {
210
+ const res = await myIdp.authenticate(identifier, password);
211
+
212
+ if (res.challenge === 'PASSWORD') return { status: 'password_required' };
213
+ if (res.challenge === 'MFA') return { status: 'mfa_required', methods: res.factors };
214
+ if (res.error === 'NOT_FOUND') return { status: 'error', code: 'account_not_found' };
215
+
216
+ return { status: 'success', session: { identifier, accessToken: res.token } };
217
+ },
218
+
219
+ // Optional methods are capabilities. Implementing `requestPasswordReset`
220
+ // is what makes "Forgot password?" appear.
221
+ async requestPasswordReset({ identifier }) {
222
+ await myIdp.sendResetLink(identifier);
223
+ return { status: 'reset_link_sent', destination: maskEmail(identifier) };
224
+ },
225
+ };
226
+
227
+ document.querySelector('uds-pattern-login-card').auth = auth;
228
+ ```
229
+
230
+ ```html
231
+ <uds-pattern-login-page logo-src="/logo.svg" logo-alt="Global Payments">
232
+ <uds-pattern-login-card></uds-pattern-login-card>
233
+ </uds-pattern-login-page>
234
+ ```
235
+
236
+ The pattern's `--uds-pattern-*` tokens ship as a **separate global stylesheet**,
237
+ so registering the elements is not on its own enough — import it once, at the
238
+ host app's entry point:
239
+
240
+ ```ts
241
+ import '@globalpayments-private/gpds-pattern-web/tokens.css';
242
+ ```
243
+
244
+ Without it every custom property is undefined, which makes each `var()` invalid
245
+ at computed-value time: the card loses its spacing, surface and typography
246
+ while still rendering and behaving correctly, so nothing errors to point at the
247
+ cause. Override the tokens by redeclaring them on `:root` *after* this import.
248
+
249
+ Listen for the outcome:
250
+
251
+ ```ts
252
+ card.addEventListener('udsLoginSuccess', e => router.go('/dashboard', e.detail.session));
253
+ ```
254
+
255
+ ### Provider capabilities
256
+
257
+ | Implement | Unlocks |
258
+ | --- | --- |
259
+ | `signIn` *(required)* | the workflow itself |
260
+ | `verifyPassword` | a dedicated password step (otherwise `signIn` is re-called with both fields) |
261
+ | `selectMfaMethod` | the MFA method picker (auto-skipped when there is only one method) |
262
+ | `verifyOtp` | security-code entry |
263
+ | `resendOtp` | the throttled "Resend code" affordance |
264
+ | `requestPasswordReset` / `resetPassword` | "Forgot password?" and the new-password step |
265
+ | `requestUsernameReminder` | "Forgot username?" |
266
+ | `signUp` | the "Create one" route |
267
+
268
+ ### Error handling
269
+
270
+ Adapters return a `UdsAuthErrorCode`, never a message. The pattern maps codes to
271
+ approved copy, so raw IdP strings never reach a user. Override any of them:
272
+
273
+ ```html
274
+ <uds-pattern-login-card config='{"labels":{"errors":{"account_not_found":"No encontramos esa cuenta."}}}'></uds-pattern-login-card>
275
+ ```
276
+
277
+ ### Validation
278
+
279
+ Form validation is `vega-form`'s, not the pattern's. Its submit handler awaits
280
+ `valid()` across every field it controls and only emits `vegaFormSubmit` once
281
+ they all pass, so an invalid step never reaches your adapter — and the field at
282
+ fault is the thing that reports it, in the design system's error presentation.
283
+
284
+ Fields declare what they need and Vega derives the rest, including the wording:
285
+
286
+ | Declared | Gives you |
287
+ | --- | --- |
288
+ | `required` | required rule, the asterisk on the label, "This field is required" |
289
+ | `type="email"` | email-format rule and "Please enter the correct email format." |
290
+
291
+ Only two checks cannot be expressed that way, so they live in
292
+ [`validation/login-validation-rules.ts`](src/components/uds-pattern-login-card/validation/login-validation-rules.ts):
293
+
294
+ - **`MatchesFieldRule`** — "confirm new password" must equal "new password". It
295
+ depends on a sibling field, so no single-field rule can state it.
296
+ - **`CompleteCodeRule`** — `required` only asks whether a value is non-empty, so
297
+ a half-typed security code would satisfy it and reach the provider.
298
+
299
+ The card still raises a banner, but only for what a *provider* returns — those
300
+ are workflow outcomes, not field errors, and they have no field to attach to.
301
+
302
+ ## Customisation: the escape-hatch ladder
303
+
304
+ Reach for the lowest level that solves your problem (architecture doc §8):
305
+
306
+ | Level | Mechanism | Use when |
307
+ | --- | --- | --- |
308
+ | 1 | Named slots (`brand`, `footer`) | injecting your own content |
309
+ | 2 | CSS custom properties (`--uds-pattern-*`, plus Vega's own) | re-theming |
310
+ | 3 | `config.auth` — a `UdsAuthProvider` | adapting to your identity platform |
311
+ | 4 | `config.control` — a `UdsLoginControlStrategy` | the course of the workflow genuinely differs |
312
+ | 5 | Composition in a layout container | rearranging the page |
313
+ | 6 | Fork | last resort; please open an issue first |
314
+
315
+ Two things that used to be on this ladder are deliberately **not** here any more
316
+ — see [ADR 0003](docs/adr/0003-aggregate-customisation-seams.md):
317
+
318
+ - **Atomic override props** (`controlled` / `step` / `loading` / `error-message`)
319
+ are gone. Customising behaviour through a handful of independent props means a
320
+ new path only works if you remember to set *all* of them; miss one and you get
321
+ a half-configured workflow that still renders. The replacement is a single
322
+ aggregate seam.
323
+ - **CSS `::part` exposure** is gone. A `::part` rule accepts arbitrary
324
+ declarations, so it can hide the error alert, remove a focus ring or break
325
+ contrast — guardrails the design system is accountable for. Custom properties
326
+ stay, because they constrain the *value* rather than handing over the
327
+ declaration.
328
+
329
+ ### `UdsLoginControlStrategy`
330
+
331
+ One object with an interceptor at every checkpoint the workflow has. Each hook
332
+ receives `next()` — the pattern's own behaviour — so you adapt a decision rather
333
+ than reimplement the machine, and a strategy that only observes cannot
334
+ accidentally change the flow.
335
+
336
+ | Checkpoint | Fires | Default |
337
+ | --- | --- | --- |
338
+ | `submit` | before the provider is consulted | calls the provider method for the step |
339
+ | `transition` | on every provider result | maps the result to a step |
340
+ | `navigate` | on every step change | performs the move |
341
+ | `failure` | on every failure | maps the error code to approved copy |
342
+ | `complete` | on success | emits `udsLoginSuccess` |
343
+
344
+ ```ts
345
+ import { goTo, stay } from '@globalpayments-private/gpds-pattern-web';
346
+
347
+ card.config = {
348
+ auth,
349
+ control: {
350
+ // Hand sign-up off to your own journey.
351
+ navigate: (ctx, next) => {
352
+ if (ctx.to === 'sign-up') {
353
+ router.push('/register');
354
+ return stay();
355
+ }
356
+ return next();
357
+ },
358
+
359
+ // Insert a consent gate ahead of the password reset.
360
+ transition: (ctx, next) => {
361
+ const outcome = next();
362
+ return outcome.type === 'goto' && outcome.step === 'reset-password' && !consented
363
+ ? goTo('check-email')
364
+ : outcome;
365
+ },
366
+ },
367
+ };
368
+ ```
369
+
370
+ Because every checkpoint lives in one object, a new path is expressed — and
371
+ reviewed — as a whole. That is the point: it pushes both us and the consumer to
372
+ reason at the workflow level, which is where a pattern library earns its keep.
373
+
374
+ ## Architecture: slimmers
375
+
376
+ The card's behaviour is split across small single-responsibility classes
377
+ (*slimmers*) that bind to the component's state declaratively, rather than
378
+ living in one large component file:
379
+
380
+ ```
381
+ src/components/uds-pattern-login-card/
382
+ uds-pattern-login-card.tsx props, events, state, render shell
383
+ slimmers/
384
+ login-workflow.slimmer.ts the state machine
385
+ resend-throttle.slimmer.ts the resend window and countdown
386
+ identity-memory.slimmer.ts remembered-identifier persistence
387
+ renderers/
388
+ login-card-renderer.tsx card chrome: heading, banner, form
389
+ login-step-renderer.tsx the body of the current step
390
+ login-field-renderer.tsx the Vega controls themselves
391
+ view-model/
392
+ login-step.view-model.ts what to show, derived in one place
393
+ login-card-handlers.ts what a renderer may ask for
394
+ ```
395
+
396
+ Rendering uses the same mechanism, in `slimmers/renderers/` — the convention
397
+ Vega itself follows. A renderer binds to the component's `viewModel` and
398
+ `handlers` with `@MapToComponentField()` and types them by indexing the
399
+ component class, so it cannot drift out of step with what the component exposes.
400
+ Renderers only read; every mutation goes back through `handlers`.
401
+
402
+ The DI framework behind this is in [`src/slimmer/`](src/slimmer/README.md),
403
+ ported from the legacy Vega design system's *Vega Slimmer* (VD-1102 / VCD-20).
404
+
405
+ It is vendored here only until it can be depended on. The framework has since
406
+ been extracted on the design-system side as `@gpds-web/slimmer`
407
+ (`packages/slimmer` in `gpds-web`), but that package is currently private and
408
+ unpublished, so this repository cannot consume it yet. When it ships,
409
+ `src/slimmer/` should be deleted in favour of the dependency — every import
410
+ already goes through `src/slimmer/index.ts`, so the swap is a dependency change
411
+ rather than a refactor. See
412
+ [ADR 0005](docs/adr/0005-patch-stencil-for-slimmers.md).
413
+
414
+ ## Testing
415
+
416
+ Two suites, with different jobs.
417
+
418
+ ```bash
419
+ npm test # spec tests
420
+ npm run test:coverage # the same suite, with coverage thresholds enforced
421
+ npm run e2e # build + serve the React host + drive it with Cypress
422
+ ```
423
+
424
+ ### Spec tests
425
+
426
+ Workflow behaviour is tested through `createMemoryAuthProvider()`, a reference
427
+ fake IdP that is also the clearest worked example of an adapter.
428
+
429
+ Spec tests assert the *contract*, not our markup — which Vega elements we
430
+ render, what we configure them with, and what we do when they report a change.
431
+ Vega's real classes are deliberately **not** registered in `newSpecPage`: that
432
+ would pull the design system's dependency graph into our unit tests and break
433
+ them whenever Vega changed something internal.
434
+
435
+ #### Coverage
436
+
437
+ `npm run test:coverage` fails the build below the thresholds in
438
+ `stencil.config.ts`. The bar is **100% of statements, branches, functions and
439
+ lines, globally, with no per-file exceptions.**
440
+
441
+ That is deliberate. A per-file relaxation hides the interesting question —
442
+ *why* is this branch unreachable? — behind a number. Every exception this
443
+ suite used to carry turned out to be either dead code or a missing test, and
444
+ none of them survived the question:
445
+
446
+ | Was excused | What it actually was |
447
+ | --- | --- |
448
+ | `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. |
449
+ | `result.methods ?? []` (×2) | `UdsAuthResult` declares `methods` as **required** on the `mfa_required` arm and `parseAuthResult` asserts it. Dead code. Deleted. |
450
+ | `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. |
451
+ | `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. |
452
+ | `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. |
453
+
454
+ No `istanbul ignore` comments were needed. If one ever is, it belongs on the
455
+ single line with a comment explaining the impossibility — not on a whole file.
456
+
457
+ ### End-to-end tests
458
+
459
+ The spec boundary — everything that depends on Vega's real classes — has to be
460
+ checked in a real browser. Doing so has caught five bugs that every spec passed
461
+ through: `vega-button[type="submit"]` being unable to submit a native ancestor
462
+ `<form>` across its shadow boundary; loose `vega-selection-tile` elements
463
+ throwing on unmount without their group ancestor; and a slimmer without an
464
+ explicit constructor rendering an entirely **empty component** while all 74
465
+ specs stayed green (now fixed at source — ADR 0005).
466
+
467
+ The Cypress suite in [`cypress/`](cypress) closes that gap, and it drives a
468
+ **React application** — [`e2e-app/`](e2e-app) — rather than a static page.
469
+
470
+ That is the point. The static demo proves the component works; a React host
471
+ proves the component is *integrable*, and React 18 is where custom elements
472
+ actually bite:
473
+
474
+ - **Object props stringify.** React 18 sets any prop it does not recognise as
475
+ an attribute, so `config={adapter}` arrives as the string
476
+ `"[object Object]"`. The config has to be assigned to the element *property*,
477
+ from a `ref`.
478
+ - **Custom events have no JSX surface.** `onUdsLoginSuccess` is not a thing;
479
+ `udsLoginStepChange` and friends need `addEventListener`.
480
+
481
+ Both live in [`e2e-app/src/useLoginCard.ts`](e2e-app/src/useLoginCard.ts), which
482
+ is as close to documentation of the React integration as it is test
483
+ scaffolding. Standing it up immediately found two bugs the static demo could
484
+ never have: the package's `exports` map pointed `import` at
485
+ `dist/esm/index.mjs`, a file the build does not emit — so **every ESM consumer
486
+ importing the package by name failed to resolve it** — and a listener effect
487
+ keyed on a ref rather than the element went deaf after a remount.
488
+
489
+ The app consumes the **built package**, not `src/`, through a `file:..`
490
+ dependency that npm installs as a symlink. There is no bundler alias, so what
491
+ Cypress drives is exactly what a consumer would install, `exports` map included
492
+ — which is why the broken entry point surfaced at all.
493
+
494
+ The static demo at `src/index.html` is untouched and still what GitHub Pages
495
+ publishes; it is now a demo rather than a test fixture.
496
+
497
+ > Vite + React rather than Next.js: SSR has no bearing on what this suite
498
+ > checks, and custom elements under Next would need `ssr: false` dynamic
499
+ > imports throughout — ceremony that tests the workaround, not the component.
500
+ > Say the word if a Next host is wanted and it can be swapped in.
501
+
502
+ ```bash
503
+ npm run e2e # one shot: build package, build app, serve, run, stop
504
+ # or, against a server you are already running:
505
+ npm run e2e-app:build
506
+ npm run serve:e2e-app &
507
+ npm run cypress:run # headless
508
+ npm run cypress:open # interactive
509
+ ```
510
+
511
+ What it covers: the full journey (identifier → password → MFA choice → code →
512
+ signed in), capability-driven rendering, the lockout branch, localisation,
513
+ control-strategy interception, and validation gating — each invalid submit
514
+ asserted to reach the adapter **zero** times, read off the host's event log.
515
+
516
+ Two things to know before writing more specs:
517
+
518
+ - **`includeShadowDom: true`** is set in `cypress.config.ts` and is not
519
+ optional: everything this library renders lives inside a shadow root.
520
+ - **Text inputs are typed into with `{ force: true }`.** Cypress 13 misreads an
521
+ `<input>` reached through two shadow roots as non-focusable, although the
522
+ same element passes `:not([disabled])`, `be.visible` and `.click()`. The
523
+ keystrokes are real; only the pre-flight check is wrong. `cy.fillField()`
524
+ already handles this.
525
+ - **`vega-input-passcode` is driven through its `vegaChange` event**, not
526
+ keystrokes. Typing into its boxes — or dispatching a bare `input` event at
527
+ them — hangs the renderer indefinitely. That reproduces on a standalone,
528
+ *uncontrolled* `vega-input-passcode` on an otherwise empty page, so it is a
529
+ Vega-side interaction with synthetic events rather than anything this pattern
530
+ introduces. `cy.fillPasscode()` wraps the workaround.
531
+
532
+ #### Linux prerequisites
533
+
534
+ Cypress needs an X server and a few shared libraries that a bare container does
535
+ not have:
536
+
537
+ ```bash
538
+ sudo apt-get install -y xvfb libgtk-3-0 libgbm1 libnotify4 libnss3 libxss1 libasound2
539
+ ```
540
+
541
+ #### CI
542
+
543
+ **Not wired up yet, deliberately.** `gpds-pattern-web` has two registered
544
+ workflows (`demo-pages.yml`, `demo-preview-cleanup.yml`), both `active`, and
545
+ **neither has ever produced a run** — including on the push to `main` that
546
+ merged them. `gpds-web` has no workflows at all and no in-repo pipeline
547
+ definition, so there is no existing Cypress CI setup to mirror. Both suites are
548
+ single commands (`npm run test:coverage`, `npm run e2e`) and are ready to
549
+ be dropped into whichever system actually runs; adding a config that silently
550
+ 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;