@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.
- package/README.md +549 -2
- package/dist/cjs/control-strategy-DhhYTbse.js +113 -0
- package/dist/cjs/gpds-pattern-web.cjs.js +24 -0
- package/dist/cjs/index-Dzm6P89U.js +1565 -0
- package/dist/cjs/index.cjs.js +180 -0
- package/dist/cjs/loader.cjs.js +12 -0
- package/dist/cjs/uds-pattern-login-card_2.cjs.entry.js +1662 -0
- package/dist/collection/collection-manifest.json +14 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.js +63 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.js +357 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.js +100 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.js +129 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.js +192 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.js +65 -0
- package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.css +154 -0
- package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.js +508 -0
- package/dist/collection/components/uds-pattern-login-card/validation/login-validation-rules.js +86 -0
- package/dist/collection/components/uds-pattern-login-card/view-model/login-card-handlers.js +1 -0
- package/dist/collection/components/uds-pattern-login-card/view-model/login-step.view-model.js +138 -0
- package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.css +37 -0
- package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.js +97 -0
- package/dist/collection/index.js +5 -0
- package/dist/collection/providers/auth-provider.js +28 -0
- package/dist/collection/providers/control-strategy.js +21 -0
- package/dist/collection/providers/login-config.js +4 -0
- package/dist/collection/providers/memory-auth-provider.js +133 -0
- package/dist/collection/providers/parse-auth-result.js +135 -0
- package/dist/collection/slimmer/common/log.js +14 -0
- package/dist/collection/slimmer/common/symbols.js +10 -0
- package/dist/collection/slimmer/common/types.js +2 -0
- package/dist/collection/slimmer/decorators/inject-slimmer.js +73 -0
- package/dist/collection/slimmer/decorators/map-to-component-field.js +41 -0
- package/dist/collection/slimmer/decorators/map-to-component-method.js +33 -0
- package/dist/collection/slimmer/decorators/slimmer.js +10 -0
- package/dist/collection/slimmer/index.js +5 -0
- package/dist/collection/utils/labels.js +81 -0
- package/dist/collection/utils/types.js +1 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +1 -0
- package/dist/components/p-DOrg55po.js +1 -0
- package/dist/components/p-_G8A9gg0.js +1 -0
- package/dist/components/uds-pattern-login-card.d.ts +11 -0
- package/dist/components/uds-pattern-login-card.js +1 -0
- package/dist/components/uds-pattern-login-page.d.ts +11 -0
- package/dist/components/uds-pattern-login-page.js +1 -0
- package/dist/docs.d.ts +480 -0
- package/dist/docs.json +549 -0
- package/dist/esm/control-strategy-DOrg55po.js +105 -0
- package/dist/esm/gpds-pattern-web.js +20 -0
- package/dist/esm/index-DsXlLyGj.js +1537 -0
- package/dist/esm/index.js +170 -0
- package/dist/esm/loader.js +10 -0
- package/dist/esm/uds-pattern-login-card_2.entry.js +1659 -0
- package/dist/gpds-pattern-web/gpds-pattern-web.css +1 -0
- package/dist/gpds-pattern-web/gpds-pattern-web.esm.js +1 -0
- package/dist/gpds-pattern-web/index.esm.js +1 -0
- package/dist/gpds-pattern-web/p-2c97d2da.entry.js +1 -0
- package/dist/gpds-pattern-web/p-DOrg55po.js +1 -0
- package/dist/gpds-pattern-web/p-DsXlLyGj.js +3 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.js +1 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.d.ts +21 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.d.ts +100 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.d.ts +44 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.d.ts +91 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.d.ts +40 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.d.ts +21 -0
- package/dist/types/components/uds-pattern-login-card/uds-pattern-login-card.d.ts +134 -0
- package/dist/types/components/uds-pattern-login-card/validation/login-validation-rules.d.ts +72 -0
- package/dist/types/components/uds-pattern-login-card/view-model/login-card-handlers.d.ts +21 -0
- package/dist/types/components/uds-pattern-login-card/view-model/login-step.view-model.d.ts +96 -0
- package/dist/types/components/uds-pattern-login-page/uds-pattern-login-page.d.ts +21 -0
- package/dist/types/components.d.ts +327 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/providers/auth-provider.d.ts +168 -0
- package/dist/types/providers/control-strategy.d.ts +144 -0
- package/dist/types/providers/login-config.d.ts +52 -0
- package/dist/types/providers/memory-auth-provider.d.ts +37 -0
- package/dist/types/providers/parse-auth-result.d.ts +8 -0
- package/dist/types/slimmer/common/log.d.ts +8 -0
- package/dist/types/slimmer/common/symbols.d.ts +10 -0
- package/dist/types/slimmer/common/types.d.ts +30 -0
- package/dist/types/slimmer/decorators/inject-slimmer.d.ts +24 -0
- package/dist/types/slimmer/decorators/map-to-component-field.d.ts +16 -0
- package/dist/types/slimmer/decorators/map-to-component-method.d.ts +11 -0
- package/dist/types/slimmer/decorators/slimmer.d.ts +10 -0
- package/dist/types/slimmer/index.d.ts +6 -0
- package/dist/types/stencil-public-runtime.d.ts +1879 -0
- package/dist/types/utils/labels.d.ts +13 -0
- package/dist/types/utils/types.d.ts +106 -0
- package/dist/types/vega.d.ts +11 -0
- package/loader/cdn.js +1 -0
- package/loader/index.cjs.js +1 -0
- package/loader/index.d.ts +24 -0
- package/loader/index.es2017.js +1 -0
- package/loader/index.js +2 -0
- package/package.json +79 -4
package/README.md
CHANGED
|
@@ -1,3 +1,550 @@
|
|
|
1
|
-
#
|
|
1
|
+
# gpds-pattern-web
|
|
2
2
|
|
|
3
|
-
|
|
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;
|