@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.
- package/README.md +567 -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 +82 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entries": [
|
|
3
|
+
"components/uds-pattern-login-card/uds-pattern-login-card.js",
|
|
4
|
+
"components/uds-pattern-login-page/uds-pattern-login-page.js"
|
|
5
|
+
],
|
|
6
|
+
"mixins": [],
|
|
7
|
+
"compiler": {
|
|
8
|
+
"name": "@stencil/core",
|
|
9
|
+
"version": "4.45.2",
|
|
10
|
+
"typescriptVersion": "5.8.3"
|
|
11
|
+
},
|
|
12
|
+
"collections": [],
|
|
13
|
+
"bundles": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
4
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else
|
|
6
|
+
for (var i = decorators.length - 1; i >= 0; i--)
|
|
7
|
+
if (d = decorators[i])
|
|
8
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
import { UdsSlimmer, MapToComponentField } from "../../../slimmer";
|
|
12
|
+
/**
|
|
13
|
+
* Persistence of the remembered identifier.
|
|
14
|
+
*
|
|
15
|
+
* Isolated because it is the one place the pattern touches storage, and storage
|
|
16
|
+
* can fail for reasons that have nothing to do with signing in — private
|
|
17
|
+
* browsing, blocked cookies, a strict enterprise policy. Those failures are
|
|
18
|
+
* swallowed deliberately: remembering an email address is a convenience, never
|
|
19
|
+
* a precondition for authenticating.
|
|
20
|
+
*/
|
|
21
|
+
export class IdentityMemorySlimmer extends UdsSlimmer {
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
}
|
|
25
|
+
/** The identifier the card should start with. */
|
|
26
|
+
initialIdentifier() {
|
|
27
|
+
return this.identifier || this.read() || '';
|
|
28
|
+
}
|
|
29
|
+
read() {
|
|
30
|
+
if (!this.rememberStorageKey) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return globalThis.localStorage?.getItem(this.rememberStorageKey) ?? '';
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Stores or clears the identifier according to the user's choice. */
|
|
41
|
+
remember(identifier, remember) {
|
|
42
|
+
if (!this.rememberStorageKey) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
if (remember) {
|
|
47
|
+
globalThis.localStorage?.setItem(this.rememberStorageKey, identifier);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
globalThis.localStorage?.removeItem(this.rememberStorageKey);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// See the class comment: persistence is best-effort by design.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
__decorate([
|
|
59
|
+
MapToComponentField()
|
|
60
|
+
], IdentityMemorySlimmer.prototype, "rememberStorageKey", void 0);
|
|
61
|
+
__decorate([
|
|
62
|
+
MapToComponentField()
|
|
63
|
+
], IdentityMemorySlimmer.prototype, "identifier", void 0);
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
4
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else
|
|
6
|
+
for (var i = decorators.length - 1; i >= 0; i--)
|
|
7
|
+
if (d = decorators[i])
|
|
8
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
import { UdsSlimmer, MapToComponentField, MapToComponentMethod } from "../../../slimmer";
|
|
12
|
+
import { fail, goTo, intercept, stay, } from "../../../providers/control-strategy";
|
|
13
|
+
import { parseAuthResult } from "../../../providers/parse-auth-result";
|
|
14
|
+
/**
|
|
15
|
+
* The login state machine.
|
|
16
|
+
*
|
|
17
|
+
* This is the pattern's core asset: the translation from "what the identity
|
|
18
|
+
* platform needs next" into "what the user sees next". Keeping it in one class,
|
|
19
|
+
* away from markup and input plumbing, is what makes the sequencing reviewable.
|
|
20
|
+
*
|
|
21
|
+
* Every decision it makes passes through the host's `controlStrategy` first, so
|
|
22
|
+
* a consumer can redirect the flow without reimplementing it.
|
|
23
|
+
*/
|
|
24
|
+
export class LoginWorkflowSlimmer extends UdsSlimmer {
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
/** The identifier being authenticated, carried across steps. */
|
|
28
|
+
this.pendingIdentifier = '';
|
|
29
|
+
}
|
|
30
|
+
// --- Lifecycle ----------------------------------------------------------
|
|
31
|
+
start() {
|
|
32
|
+
this.step = this.initialStep;
|
|
33
|
+
this.pendingIdentifier = this.memory.initialIdentifier();
|
|
34
|
+
this.resetStepState();
|
|
35
|
+
}
|
|
36
|
+
/** Returns the workflow to its first step and clears all collected input. */
|
|
37
|
+
async restart() {
|
|
38
|
+
this.methods = [];
|
|
39
|
+
this.errorText = undefined;
|
|
40
|
+
this.destination = undefined;
|
|
41
|
+
this.unlockMinutes = undefined;
|
|
42
|
+
this.otpCount = undefined;
|
|
43
|
+
this.resend.stop();
|
|
44
|
+
this.applyTransition(goTo(this.initialStep), 'reset');
|
|
45
|
+
}
|
|
46
|
+
// --- Capabilities -------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* Which affordances to render. The presence of an optional provider method
|
|
49
|
+
* *is* the configuration, so a route can never be offered for something the
|
|
50
|
+
* backend cannot service.
|
|
51
|
+
*/
|
|
52
|
+
get capabilities() {
|
|
53
|
+
const a = this.auth;
|
|
54
|
+
return {
|
|
55
|
+
signUp: typeof a?.signUp === 'function',
|
|
56
|
+
reset: typeof a?.requestPasswordReset === 'function',
|
|
57
|
+
username: typeof a?.requestUsernameReminder === 'function',
|
|
58
|
+
resend: typeof a?.resendOtp === 'function',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// --- Navigation ---------------------------------------------------------
|
|
62
|
+
/** In-workflow navigation, triggered by a link in the card. */
|
|
63
|
+
navigateTo(step) {
|
|
64
|
+
this.errorText = undefined;
|
|
65
|
+
this.applyTransition(goTo(step), 'link');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Performs a transition, giving the strategy its say on every step change —
|
|
69
|
+
* including the ones the workflow initiates for itself.
|
|
70
|
+
*/
|
|
71
|
+
applyTransition(transition, cause) {
|
|
72
|
+
if (transition.type === 'stay') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (transition.type === 'fail') {
|
|
76
|
+
this.showError(transition.code, transition.message);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const decided = intercept(this.controlStrategy?.navigate, { from: this.step, to: transition.step, cause }, () => transition);
|
|
80
|
+
// A strategy may answer a navigation with a different kind of outcome
|
|
81
|
+
// entirely — stay put, or surface a failure instead of moving.
|
|
82
|
+
if (decided.type === 'stay') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (decided.type === 'fail') {
|
|
86
|
+
this.showError(decided.code, decided.message);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.setStep(decided.step);
|
|
90
|
+
}
|
|
91
|
+
setStep(next) {
|
|
92
|
+
const previousStep = this.step;
|
|
93
|
+
this.step = next;
|
|
94
|
+
this.resetStepState();
|
|
95
|
+
if (previousStep !== next) {
|
|
96
|
+
this.udsLoginStepChange.emit({ step: next, previousStep });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Clears per-step input, seeding the identifier field where appropriate. */
|
|
100
|
+
resetStepState() {
|
|
101
|
+
const seedsIdentifier = this.step === 'identifier' || this.step === 'forgot-password';
|
|
102
|
+
this.values = seedsIdentifier && this.pendingIdentifier ? { identifier: this.pendingIdentifier } : {};
|
|
103
|
+
}
|
|
104
|
+
/** How many characters the passcode entry should render. */
|
|
105
|
+
get codeLength() {
|
|
106
|
+
return Math.max(1, this.otpCount ?? this.otpLength);
|
|
107
|
+
}
|
|
108
|
+
// --- Result translation -------------------------------------------------
|
|
109
|
+
/**
|
|
110
|
+
* Translates a provider result into the next step. This inversion is what
|
|
111
|
+
* makes the pattern a pattern: the identity platform reports what it needs,
|
|
112
|
+
* and the card — not the consumer — decides what that looks like.
|
|
113
|
+
*/
|
|
114
|
+
applyResult(result) {
|
|
115
|
+
this.errorText = undefined;
|
|
116
|
+
const transition = intercept(this.controlStrategy?.transition, { result, from: this.step }, () => this.defaultTransitionFor(result));
|
|
117
|
+
// Side effects that belong to the result rather than the destination are
|
|
118
|
+
// staged before the move, so the step renders with its data already in place.
|
|
119
|
+
this.stageResultState(result);
|
|
120
|
+
if (result.status === 'mfa_required' && this.methods.length === 1 && transition.type === 'stay') {
|
|
121
|
+
// A single option is not a choice — skip the picker.
|
|
122
|
+
void this.selectMfaMethod(this.methods[0].id);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.applyTransition(transition, 'auto');
|
|
126
|
+
if (result.status === 'success' && transition.type === 'goto' && transition.step === 'success') {
|
|
127
|
+
intercept(this.controlStrategy?.complete, { session: result.session }, () => this.udsLoginSuccess.emit({ session: result.session }));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
defaultTransitionFor(result) {
|
|
131
|
+
switch (result.status) {
|
|
132
|
+
case 'success':
|
|
133
|
+
return goTo('success');
|
|
134
|
+
case 'password_required':
|
|
135
|
+
return goTo('password');
|
|
136
|
+
case 'mfa_required':
|
|
137
|
+
// Staying signals "the picker is not needed"; `applyResult` turns that
|
|
138
|
+
// into an auto-selection when the provider offered exactly one method.
|
|
139
|
+
return result.methods.length === 1 ? stay() : goTo('mfa-method');
|
|
140
|
+
case 'otp_required':
|
|
141
|
+
return goTo('otp');
|
|
142
|
+
case 'password_reset_required':
|
|
143
|
+
return goTo('reset-password');
|
|
144
|
+
case 'reset_link_sent':
|
|
145
|
+
case 'username_sent':
|
|
146
|
+
return goTo('check-email');
|
|
147
|
+
case 'locked':
|
|
148
|
+
return goTo('locked');
|
|
149
|
+
case 'error':
|
|
150
|
+
return fail(result.code, result.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Captures the data a result carries, independent of where the flow goes. */
|
|
154
|
+
stageResultState(result) {
|
|
155
|
+
switch (result.status) {
|
|
156
|
+
case 'mfa_required':
|
|
157
|
+
this.methods = result.methods;
|
|
158
|
+
return;
|
|
159
|
+
case 'otp_required':
|
|
160
|
+
this.otpMethodLabel = result.method?.label;
|
|
161
|
+
this.otpCount = result.codeLength ?? this.otpLength;
|
|
162
|
+
this.resend.start(result.resendAfterMs);
|
|
163
|
+
return;
|
|
164
|
+
case 'reset_link_sent':
|
|
165
|
+
case 'username_sent':
|
|
166
|
+
this.destination = result.destination;
|
|
167
|
+
return;
|
|
168
|
+
case 'locked':
|
|
169
|
+
this.unlockMinutes = result.unlockAfterMs ? Math.ceil(result.unlockAfterMs / 60000) : undefined;
|
|
170
|
+
return;
|
|
171
|
+
default:
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// --- Failures -----------------------------------------------------------
|
|
176
|
+
showError(code, message) {
|
|
177
|
+
const outcome = intercept(this.controlStrategy?.failure, { code, step: this.step, message }, () => ({
|
|
178
|
+
text: this.copy.errors[code] ?? this.copy.errors.unknown,
|
|
179
|
+
}));
|
|
180
|
+
this.errorText = outcome.text;
|
|
181
|
+
this.udsLoginError.emit({ code, step: this.step, message });
|
|
182
|
+
if (outcome.transition) {
|
|
183
|
+
this.applyTransition(outcome.transition, 'auto');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// --- Provider calls -----------------------------------------------------
|
|
187
|
+
/**
|
|
188
|
+
* Wraps a provider call with the in-flight guard and the network catch.
|
|
189
|
+
*
|
|
190
|
+
* `busy` is released *before* the result is applied, because applying a
|
|
191
|
+
* result may legitimately chain another provider call (auto-selecting a lone
|
|
192
|
+
* MFA method, for example). Holding the guard across `applyResult` would make
|
|
193
|
+
* that continuation a no-op and strand the user on the previous step. The
|
|
194
|
+
* chained call re-acquires the guard synchronously, so no un-busy frame is
|
|
195
|
+
* ever painted.
|
|
196
|
+
*
|
|
197
|
+
* Every provider response is validated here, at the one point where data
|
|
198
|
+
* crosses into the pattern from a consumer's adapter. See `parseAuthResult`.
|
|
199
|
+
*/
|
|
200
|
+
async run(call, source) {
|
|
201
|
+
if (this.busy) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.busy = true;
|
|
205
|
+
let result;
|
|
206
|
+
try {
|
|
207
|
+
result = parseAuthResult(await call(), `auth.${source}`);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
this.busy = false;
|
|
211
|
+
this.showError('network');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.busy = false;
|
|
215
|
+
this.applyResult(result);
|
|
216
|
+
}
|
|
217
|
+
async selectMfaMethod(methodId) {
|
|
218
|
+
if (!this.auth?.selectMfaMethod) {
|
|
219
|
+
this.showError('unknown');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
await this.run(() => this.auth.selectMfaMethod({ methodId }), 'selectMfaMethod');
|
|
223
|
+
}
|
|
224
|
+
async resendCode() {
|
|
225
|
+
if (!this.resend.canResend || !this.auth?.resendOtp) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await this.run(() => this.auth.resendOtp(), 'resendOtp');
|
|
229
|
+
}
|
|
230
|
+
// --- Submission ---------------------------------------------------------
|
|
231
|
+
/**
|
|
232
|
+
* Submits the current step.
|
|
233
|
+
*
|
|
234
|
+
* The strategy wraps the whole provider call, so a consumer can gate it
|
|
235
|
+
* (CAPTCHA, device check, consent) or answer it outright without the pattern
|
|
236
|
+
* needing a dedicated prop for each case.
|
|
237
|
+
*/
|
|
238
|
+
async submit() {
|
|
239
|
+
if (this.busy) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const step = this.step;
|
|
243
|
+
const values = { ...this.values };
|
|
244
|
+
this.rememberIdentifierIfRequested(step);
|
|
245
|
+
await this.run(() => intercept(this.controlStrategy?.submit, { step, values, identifier: this.pendingIdentifier }, () => this.callProvider(step)), `submit(${step})`);
|
|
246
|
+
}
|
|
247
|
+
rememberIdentifierIfRequested(step) {
|
|
248
|
+
if (step !== 'identifier') {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const identifier = this.str('identifier').trim();
|
|
252
|
+
this.pendingIdentifier = identifier;
|
|
253
|
+
this.memory.remember(identifier, this.values.rememberIdentifier === true);
|
|
254
|
+
}
|
|
255
|
+
callProvider(step) {
|
|
256
|
+
const auth = this.auth;
|
|
257
|
+
if (!auth) {
|
|
258
|
+
return Promise.resolve({ status: 'error', code: 'unknown' });
|
|
259
|
+
}
|
|
260
|
+
const unsupported = Promise.resolve({ status: 'error', code: 'unknown' });
|
|
261
|
+
switch (step) {
|
|
262
|
+
case 'identifier':
|
|
263
|
+
return auth.signIn({
|
|
264
|
+
identifier: this.pendingIdentifier,
|
|
265
|
+
rememberIdentifier: this.values.rememberIdentifier === true,
|
|
266
|
+
});
|
|
267
|
+
case 'password': {
|
|
268
|
+
const password = this.str('password');
|
|
269
|
+
const identifier = this.pendingIdentifier;
|
|
270
|
+
return auth.verifyPassword
|
|
271
|
+
? auth.verifyPassword({ identifier, password })
|
|
272
|
+
: auth.signIn({ identifier, password });
|
|
273
|
+
}
|
|
274
|
+
case 'mfa-method':
|
|
275
|
+
return auth.selectMfaMethod?.({ methodId: this.str('mfaMethod') }) ?? unsupported;
|
|
276
|
+
case 'otp':
|
|
277
|
+
return auth.verifyOtp?.({ code: this.str('code') }) ?? unsupported;
|
|
278
|
+
case 'forgot-password':
|
|
279
|
+
return auth.requestPasswordReset?.({ identifier: this.str('identifier').trim() }) ?? unsupported;
|
|
280
|
+
case 'forgot-username':
|
|
281
|
+
return auth.requestUsernameReminder?.({ email: this.str('email').trim() }) ?? unsupported;
|
|
282
|
+
case 'reset-password':
|
|
283
|
+
return auth.resetPassword?.({ password: this.str('password') }) ?? unsupported;
|
|
284
|
+
case 'sign-up':
|
|
285
|
+
return (auth.signUp?.({
|
|
286
|
+
firstName: this.str('firstName'),
|
|
287
|
+
lastName: this.str('lastName'),
|
|
288
|
+
identifier: this.str('identifier').trim(),
|
|
289
|
+
}) ?? unsupported);
|
|
290
|
+
default:
|
|
291
|
+
return unsupported;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
str(name) {
|
|
295
|
+
return String(this.values[name] ?? '');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
__decorate([
|
|
299
|
+
MapToComponentField()
|
|
300
|
+
], LoginWorkflowSlimmer.prototype, "resend", void 0);
|
|
301
|
+
__decorate([
|
|
302
|
+
MapToComponentField()
|
|
303
|
+
], LoginWorkflowSlimmer.prototype, "memory", void 0);
|
|
304
|
+
__decorate([
|
|
305
|
+
MapToComponentField()
|
|
306
|
+
], LoginWorkflowSlimmer.prototype, "udsLoginStepChange", void 0);
|
|
307
|
+
__decorate([
|
|
308
|
+
MapToComponentField()
|
|
309
|
+
], LoginWorkflowSlimmer.prototype, "udsLoginError", void 0);
|
|
310
|
+
__decorate([
|
|
311
|
+
MapToComponentField()
|
|
312
|
+
], LoginWorkflowSlimmer.prototype, "udsLoginSuccess", void 0);
|
|
313
|
+
__decorate([
|
|
314
|
+
MapToComponentField()
|
|
315
|
+
], LoginWorkflowSlimmer.prototype, "auth", void 0);
|
|
316
|
+
__decorate([
|
|
317
|
+
MapToComponentField()
|
|
318
|
+
], LoginWorkflowSlimmer.prototype, "controlStrategy", void 0);
|
|
319
|
+
__decorate([
|
|
320
|
+
MapToComponentField()
|
|
321
|
+
], LoginWorkflowSlimmer.prototype, "initialStep", void 0);
|
|
322
|
+
__decorate([
|
|
323
|
+
MapToComponentField()
|
|
324
|
+
], LoginWorkflowSlimmer.prototype, "otpLength", void 0);
|
|
325
|
+
__decorate([
|
|
326
|
+
MapToComponentField()
|
|
327
|
+
], LoginWorkflowSlimmer.prototype, "copy", void 0);
|
|
328
|
+
__decorate([
|
|
329
|
+
MapToComponentField({ writable: true })
|
|
330
|
+
], LoginWorkflowSlimmer.prototype, "step", void 0);
|
|
331
|
+
__decorate([
|
|
332
|
+
MapToComponentField({ writable: true })
|
|
333
|
+
], LoginWorkflowSlimmer.prototype, "busy", void 0);
|
|
334
|
+
__decorate([
|
|
335
|
+
MapToComponentField({ writable: true })
|
|
336
|
+
], LoginWorkflowSlimmer.prototype, "errorText", void 0);
|
|
337
|
+
__decorate([
|
|
338
|
+
MapToComponentField({ writable: true })
|
|
339
|
+
], LoginWorkflowSlimmer.prototype, "methods", void 0);
|
|
340
|
+
__decorate([
|
|
341
|
+
MapToComponentField({ writable: true })
|
|
342
|
+
], LoginWorkflowSlimmer.prototype, "otpMethodLabel", void 0);
|
|
343
|
+
__decorate([
|
|
344
|
+
MapToComponentField({ writable: true })
|
|
345
|
+
], LoginWorkflowSlimmer.prototype, "destination", void 0);
|
|
346
|
+
__decorate([
|
|
347
|
+
MapToComponentField({ writable: true })
|
|
348
|
+
], LoginWorkflowSlimmer.prototype, "unlockMinutes", void 0);
|
|
349
|
+
__decorate([
|
|
350
|
+
MapToComponentField({ writable: true })
|
|
351
|
+
], LoginWorkflowSlimmer.prototype, "values", void 0);
|
|
352
|
+
__decorate([
|
|
353
|
+
MapToComponentMethod('componentWillLoad')
|
|
354
|
+
], LoginWorkflowSlimmer.prototype, "start", null);
|
|
355
|
+
__decorate([
|
|
356
|
+
MapToComponentMethod('reset')
|
|
357
|
+
], LoginWorkflowSlimmer.prototype, "restart", null);
|
package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
4
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else
|
|
6
|
+
for (var i = decorators.length - 1; i >= 0; i--)
|
|
7
|
+
if (d = decorators[i])
|
|
8
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
import { h } from "@stencil/core";
|
|
12
|
+
import { UdsSlimmer, InjectSlimmer, MapToComponentField } from "../../../../slimmer";
|
|
13
|
+
import { LoginStepRenderer } from "./login-step-renderer";
|
|
14
|
+
/**
|
|
15
|
+
* Renders the card's body — heading, description, error, form and the terminal
|
|
16
|
+
* outcome — and delegates the step's own controls to {@link LoginStepRenderer}.
|
|
17
|
+
*
|
|
18
|
+
* ## Why a renderer slimmer, and not the `view/` modules this replaces
|
|
19
|
+
*
|
|
20
|
+
* This is the same Slimmer mechanism the stateful layer already uses, pointed
|
|
21
|
+
* at rendering: one class, one responsibility, bound to the component by field
|
|
22
|
+
* mapping rather than by parameter passing. Vega applies the convention the
|
|
23
|
+
* same way — `slimmers/renderers/<name>-renderer.tsx`, a class ending in
|
|
24
|
+
* `Renderer`, exposing `render()` — so a reader moving between the design
|
|
25
|
+
* system and the pattern library meets one structure, not two.
|
|
26
|
+
*
|
|
27
|
+
* The gain over free functions is the binding. A free function has to be handed
|
|
28
|
+
* everything it needs on each call, so its signature grows with its appetite
|
|
29
|
+
* and callers thread arguments through layers that never use them — which is
|
|
30
|
+
* exactly what the old `StepViewModel` parameter had become.
|
|
31
|
+
* `@MapToComponentField()` reaches the component's `viewModel` and `handlers`
|
|
32
|
+
* directly, and types them by indexing the component class, so a renderer
|
|
33
|
+
* cannot drift out of step with what the component actually exposes: rename the
|
|
34
|
+
* getter and this stops compiling.
|
|
35
|
+
*
|
|
36
|
+
* Renderers only read. Every mutation goes back through `handlers`, into the
|
|
37
|
+
* workflow slimmer, and through the control strategy's checkpoints.
|
|
38
|
+
*
|
|
39
|
+
* ## What stays behind in the component
|
|
40
|
+
*
|
|
41
|
+
* `render()` itself and both `<slot>` elements remain in `UdsPatternLoginCard`. Stencil
|
|
42
|
+
* reads slots out of the component's source statically when it compiles, so a
|
|
43
|
+
* slot that only existed inside a slimmer would never be registered.
|
|
44
|
+
*/
|
|
45
|
+
export class LoginCardRenderer extends UdsSlimmer {
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
this.steps = new LoginStepRenderer();
|
|
49
|
+
}
|
|
50
|
+
render() {
|
|
51
|
+
const vm = this.viewModel;
|
|
52
|
+
return [
|
|
53
|
+
h("h1", { class: "card__heading", id: "uds-pattern-login-heading", tabindex: "-1" }, vm.heading),
|
|
54
|
+
...(vm.description ? [h("p", { class: "card__description" }, vm.description)] : []),
|
|
55
|
+
// `vega-banner` carries the design system's error presentation — icon,
|
|
56
|
+
// colour, contrast and live-region semantics — so the pattern does not
|
|
57
|
+
// maintain a parallel version that can drift away from it.
|
|
58
|
+
...(vm.errorText
|
|
59
|
+
? [h("vega-banner", { class: "alert", type: "error", content: vm.errorText, isOpen: true, showCloseButton: false })]
|
|
60
|
+
: []),
|
|
61
|
+
...(vm.isTerminal ? this.terminalBody(vm) : [this.form()]),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
form() {
|
|
65
|
+
return (
|
|
66
|
+
// `vega-form` rather than a native `<form>`: a `vega-button[type=submit]`
|
|
67
|
+
// lives in its own shadow root, so it cannot submit a native ancestor
|
|
68
|
+
// form — the click would silently do nothing. Vega's fields and buttons
|
|
69
|
+
// coordinate submission and validation through this element instead.
|
|
70
|
+
//
|
|
71
|
+
// It is also the validation gate. Its submit handler awaits `valid()`
|
|
72
|
+
// across every field it controls and only emits `vegaFormSubmit` once
|
|
73
|
+
// they all pass, so the step below never has to pre-check anything.
|
|
74
|
+
//
|
|
75
|
+
// `disableBlurValidation` suppresses validation on blur specifically.
|
|
76
|
+
// Without it, clicking Submit blurs the field underneath it, an error
|
|
77
|
+
// message appears, the button moves down and the click lands on whatever
|
|
78
|
+
// took its place — so the first click is swallowed. Errors still appear
|
|
79
|
+
// on submit, and still clear as the user types.
|
|
80
|
+
h("vega-form", { class: "form", useNativeForm: true, disableBlurValidation: true, onVegaFormSubmit: (event) => this.handlers.onSubmit(event) }, h("div", { class: "form__body" }, this.steps.render())));
|
|
81
|
+
}
|
|
82
|
+
/** Terminal steps render no form; `check-email` still offers a way back. */
|
|
83
|
+
terminalBody(vm) {
|
|
84
|
+
if (vm.step !== 'check-email') {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
h("p", { class: "prose" }, h("vega-button-link", { class: "link", label: vm.copy.back, eventful: true, onVegaClick: (event) => this.handlers.onNavigate('identifier')(event) })),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
__decorate([
|
|
93
|
+
InjectSlimmer()
|
|
94
|
+
], LoginCardRenderer.prototype, "steps", void 0);
|
|
95
|
+
__decorate([
|
|
96
|
+
MapToComponentField()
|
|
97
|
+
], LoginCardRenderer.prototype, "viewModel", void 0);
|
|
98
|
+
__decorate([
|
|
99
|
+
MapToComponentField()
|
|
100
|
+
], LoginCardRenderer.prototype, "handlers", void 0);
|
package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
4
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else
|
|
6
|
+
for (var i = decorators.length - 1; i >= 0; i--)
|
|
7
|
+
if (d = decorators[i])
|
|
8
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
import { h } from "@stencil/core";
|
|
12
|
+
import { UdsSlimmer, MapToComponentField } from "../../../../slimmer";
|
|
13
|
+
import { CompleteCodeRule, MatchesFieldRule } from "../../validation/login-validation-rules";
|
|
14
|
+
import { formatLabel } from "../../../../utils/labels";
|
|
15
|
+
/**
|
|
16
|
+
* Renders the individual form controls, composed from Vega design-system
|
|
17
|
+
* elements rather than raw HTML.
|
|
18
|
+
*
|
|
19
|
+
* ## Why these are Vega elements and not our own markup
|
|
20
|
+
*
|
|
21
|
+
* A pattern's job is workflow choreography; the look, the focus ring, the label
|
|
22
|
+
* association, the error presentation and the contrast ratios are the design
|
|
23
|
+
* system's job. Hand-rolling an `<input>` here would fork all of that: the DS
|
|
24
|
+
* could ship an accessibility fix to `vega-input` and this card would never
|
|
25
|
+
* receive it. Composing the DS element instead means the pattern inherits those
|
|
26
|
+
* guarantees — and keeps inheriting them.
|
|
27
|
+
*
|
|
28
|
+
* It is the same principle that led us to withdraw `::part` (ADR 0003), applied
|
|
29
|
+
* from the inside: we do not override design-system guardrails, and we do not
|
|
30
|
+
* quietly re-implement them either.
|
|
31
|
+
*
|
|
32
|
+
* Note how much behaviour this removes. `vega-input` carries its own password
|
|
33
|
+
* reveal affordance, so the card no longer tracks a `revealSecret` flag or
|
|
34
|
+
* renders an eye icon.
|
|
35
|
+
*/
|
|
36
|
+
export class LoginFieldRenderer extends UdsSlimmer {
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
/**
|
|
40
|
+
* Rule instances are built once and kept.
|
|
41
|
+
*
|
|
42
|
+
* `validationRules` is a watched Vega prop, so handing it a freshly built
|
|
43
|
+
* array on every render would re-register the rules each time any unrelated
|
|
44
|
+
* state moved. These close over `this.viewModel`, which the slimmer re-reads
|
|
45
|
+
* from the component, so a single stable instance always evaluates against
|
|
46
|
+
* current state.
|
|
47
|
+
*/
|
|
48
|
+
this.matchRules = new Map();
|
|
49
|
+
this.completeCode = [
|
|
50
|
+
new CompleteCodeRule(() => this.viewModel.otpLength, count => formatLabel(this.viewModel.copy.otpIncomplete, { count })),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/** One rule per pairing, memoised so the array identity is stable per field. */
|
|
54
|
+
matchRuleFor(sibling) {
|
|
55
|
+
let rule = this.matchRules.get(sibling);
|
|
56
|
+
if (!rule) {
|
|
57
|
+
rule = [
|
|
58
|
+
new MatchesFieldRule(() => this.viewModel.valueOf(sibling), () => this.viewModel.copy.errors.passwords_do_not_match),
|
|
59
|
+
];
|
|
60
|
+
this.matchRules.set(sibling, rule);
|
|
61
|
+
}
|
|
62
|
+
return rule;
|
|
63
|
+
}
|
|
64
|
+
/** A labelled text input bound to a collected value. */
|
|
65
|
+
textField(options) {
|
|
66
|
+
const vm = this.viewModel;
|
|
67
|
+
const { name, label, type = 'text', autocomplete, mustMatch } = options;
|
|
68
|
+
return (h("vega-input", { key: name, class: "field", name: name, label: label, type: type, autocomplete: autocomplete, value: vm.valueOf(name), disabled: vm.busy, showClearIcon: false,
|
|
69
|
+
// `required` and `type="email"` are the whole of this field's
|
|
70
|
+
// validation: Vega derives the rules, the messages and the error
|
|
71
|
+
// presentation from them, and `vega-form` will not submit until they
|
|
72
|
+
// pass. `autoValidation` is what keeps the form's own `isValid` current
|
|
73
|
+
// between submits, and lets an error clear as the user corrects it.
|
|
74
|
+
required: true, autoValidation: true, validationRules: mustMatch ? this.matchRuleFor(mustMatch) : undefined, onVegaChange: (event) => this.handlers.onValueChange(name, event.detail) }));
|
|
75
|
+
}
|
|
76
|
+
/** The passcode entry for the OTP step. */
|
|
77
|
+
passcodeField() {
|
|
78
|
+
const vm = this.viewModel;
|
|
79
|
+
return (h("vega-input-passcode", { class: "field", name: "code", label: vm.copy.otpFieldLabel, length: vm.otpLength, inputType: "numeric", disabled: vm.busy,
|
|
80
|
+
// `required` only asks whether the value is non-empty, so a half-typed
|
|
81
|
+
// code would pass it; `CompleteCodeRule` is what holds the submit until
|
|
82
|
+
// every box is filled.
|
|
83
|
+
required: true, autoValidation: true, validationRules: this.completeCode, value: vm.valueOf('code'), onVegaChange: (event) => this.handlers.onValueChange('code', event.detail) }));
|
|
84
|
+
}
|
|
85
|
+
/** A checkbox bound to a collected boolean. */
|
|
86
|
+
checkbox(name, label) {
|
|
87
|
+
const vm = this.viewModel;
|
|
88
|
+
return (h("vega-checkbox", { class: "field", name: name, identifier: name, checked: vm.isChecked(name), disabled: vm.busy, onVegaUserChange: (event) => this.handlers.onValueChange(name, event.detail !== false) }, label));
|
|
89
|
+
}
|
|
90
|
+
/** The step's primary action. */
|
|
91
|
+
submit(label) {
|
|
92
|
+
const vm = this.viewModel;
|
|
93
|
+
return h("vega-button", { class: "submit", type: "submit", variant: "primary", block: true, label: label, loading: vm.busy });
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* An inline textual action.
|
|
97
|
+
*
|
|
98
|
+
* `eventful` stops `vega-button-link` from navigating on its own so the
|
|
99
|
+
* workflow — not the browser — decides what the click means.
|
|
100
|
+
*/
|
|
101
|
+
link(label, onClick) {
|
|
102
|
+
return (h("vega-button-link", { class: "link", label: label, eventful: true, disabled: this.viewModel.busy, onVegaClick: (event) => onClick(event) }));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The MFA method chooser, as a design-system selection-tile group.
|
|
106
|
+
*
|
|
107
|
+
* The group — not the individual tiles — is the component Vega expects to be
|
|
108
|
+
* composed: it owns the radio `name`, the selected `value`, the change event
|
|
109
|
+
* and the group's accessible labelling. Rendering loose `vega-selection-tile`
|
|
110
|
+
* elements inside a plain `role="radiogroup"` div looked correct and even
|
|
111
|
+
* behaved correctly, but threw inside Vega's runtime when the tiles
|
|
112
|
+
* unmounted, because each tile reaches for a group ancestor that was not
|
|
113
|
+
* there.
|
|
114
|
+
*/
|
|
115
|
+
methodGroup(methods, label) {
|
|
116
|
+
const vm = this.viewModel;
|
|
117
|
+
return (h("vega-selection-tile-group", { class: "options", name: "mfaMethod", label: label, selectType: "radio", layout: "vertical", disabled: vm.busy,
|
|
118
|
+
// No `autoValidation` counterpart on the group — a tile selection has
|
|
119
|
+
// no intermediate state to validate against, so the submit gate is
|
|
120
|
+
// the only moment the rule can fire.
|
|
121
|
+
required: true, value: vm.valueOf('mfaMethod'), onVegaChange: (event) => this.handlers.onValueChange('mfaMethod', event.detail) }, methods.map(method => (h("vega-selection-tile", { key: method.id, class: "option", value: method.id, titleText: method.label, note: method.description, selectType: "radio", checked: vm.valueOf('mfaMethod') === method.id, disabled: method.disabled === true || vm.busy })))));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
__decorate([
|
|
125
|
+
MapToComponentField()
|
|
126
|
+
], LoginFieldRenderer.prototype, "viewModel", void 0);
|
|
127
|
+
__decorate([
|
|
128
|
+
MapToComponentField()
|
|
129
|
+
], LoginFieldRenderer.prototype, "handlers", void 0);
|