@idp.global/catalog 3.1.0 → 3.3.0

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.
@@ -0,0 +1,267 @@
1
+ import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
2
+ import { idpElementStyles } from './tokens.js';
3
+ import './idp-auth-card.js';
4
+ import './idp-option-row.js';
5
+ import './idp-otp-input.js';
6
+ import './idp-status-icon.js';
7
+ import './idp-button.js';
8
+ import type { IIdpOtpChangeEventDetail } from './idp-otp-input.js';
9
+
10
+ export type TIdpSecureAccountStage = 'choice' | 'totp' | 'backup-codes' | 'done';
11
+
12
+ declare global {
13
+ interface HTMLElementTagNameMap {
14
+ 'idp-secure-account-card': IdpSecureAccountCard;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Post-registration security enrollment card. Purely presentational: the host
20
+ * runs the WebAuthn/TOTP requests and drives `stage`, reacting to
21
+ * idp-secure-passkey, idp-secure-totp, idp-secure-totp-verify, idp-secure-skip
22
+ * and idp-secure-continue.
23
+ */
24
+ @customElement('idp-secure-account-card')
25
+ export class IdpSecureAccountCard extends DeesElement {
26
+ public static demo = () => html`
27
+ <div style="background: var(--sh-bg, #f5f5f7); padding: 40px; display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
28
+ <idp-secure-account-card stage="choice"></idp-secure-account-card>
29
+ <idp-secure-account-card stage="totp" totpSecret="JBSWY3DPEHPK3PXP" totpOtpauthUrl="otpauth://totp/idp.global:alex%40lossless.com?secret=JBSWY3DPEHPK3PXP&issuer=idp.global"></idp-secure-account-card>
30
+ <idp-secure-account-card stage="backup-codes" .backupCodes=${['1111-2222', '3333-4444', '5555-6666', '7777-8888', '9999-0000', '1234-5678']}></idp-secure-account-card>
31
+ <idp-secure-account-card stage="done" doneMethod="passkey"></idp-secure-account-card>
32
+ </div>
33
+ `;
34
+ public static demoGroups = ['idp.global v5 auth'];
35
+
36
+ @property({ type: String, reflect: true })
37
+ public accessor stage: TIdpSecureAccountStage = 'choice';
38
+
39
+ /** disables actions while the host performs a request */
40
+ @property({ type: Boolean })
41
+ public accessor busy = false;
42
+
43
+ @property({ type: String })
44
+ public accessor errorText = '';
45
+
46
+ /** manual-entry TOTP secret shown during the totp stage */
47
+ @property({ type: String })
48
+ public accessor totpSecret = '';
49
+
50
+ /** otpauth:// URL; rendered as a tappable link for phone authenticators */
51
+ @property({ type: String })
52
+ public accessor totpOtpauthUrl = '';
53
+
54
+ /** one-time backup codes shown after successful TOTP verification */
55
+ @property({ type: Array })
56
+ public accessor backupCodes: string[] = [];
57
+
58
+ /** which method completed, for the done stage copy */
59
+ @property({ type: String })
60
+ public accessor doneMethod: 'passkey' | 'totp' | '' = '';
61
+
62
+ public static styles = [
63
+ ...idpElementStyles,
64
+ css`
65
+ :host {
66
+ display: block;
67
+ font-family: var(--idp-font);
68
+ }
69
+ .option-list {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 10px;
73
+ margin-top: 16px;
74
+ }
75
+ .skip-row {
76
+ margin-top: 16px;
77
+ text-align: center;
78
+ font-size: 13px;
79
+ }
80
+ .skip-row a {
81
+ color: var(--idp-fg-3);
82
+ cursor: pointer;
83
+ text-decoration: none;
84
+ }
85
+ .skip-row a:hover {
86
+ color: var(--idp-fg-2);
87
+ text-decoration: underline;
88
+ }
89
+ .error-note {
90
+ margin-top: 14px;
91
+ padding: 9px 12px;
92
+ background: color-mix(in srgb, #ff3b30 8%, transparent);
93
+ border: 1px solid color-mix(in srgb, #ff3b30 30%, transparent);
94
+ border-radius: 11px;
95
+ font-size: 12.5px;
96
+ color: #d70015;
97
+ }
98
+ .totp-block {
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 12px;
102
+ margin-top: 16px;
103
+ }
104
+ .code-block {
105
+ padding: 10px 12px;
106
+ background: var(--idp-bg);
107
+ border: 1px solid var(--idp-border);
108
+ border-radius: 11px;
109
+ font-family: ui-monospace, 'SF Mono', monospace;
110
+ font-size: 12px;
111
+ word-break: break-all;
112
+ user-select: all;
113
+ }
114
+ .code-label {
115
+ font-size: 11.5px;
116
+ color: var(--idp-fg-3);
117
+ margin-bottom: 4px;
118
+ }
119
+ .otp-row {
120
+ display: flex;
121
+ justify-content: center;
122
+ margin-top: 4px;
123
+ }
124
+ .backup-grid {
125
+ display: grid;
126
+ grid-template-columns: 1fr 1fr;
127
+ gap: 8px;
128
+ margin-top: 16px;
129
+ }
130
+ .backup-code {
131
+ padding: 8px 10px;
132
+ background: var(--idp-bg);
133
+ border: 1px solid var(--idp-border);
134
+ border-radius: 9px;
135
+ font-family: ui-monospace, 'SF Mono', monospace;
136
+ font-size: 12.5px;
137
+ text-align: center;
138
+ user-select: all;
139
+ }
140
+ .status-block {
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ gap: 14px;
145
+ padding: 18px 0 6px;
146
+ text-align: center;
147
+ }
148
+ .status-text {
149
+ font-size: 13px;
150
+ color: var(--idp-fg-2);
151
+ line-height: 1.5;
152
+ max-width: 280px;
153
+ }
154
+ .actions {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 10px;
158
+ margin-top: 18px;
159
+ }
160
+ `,
161
+ ];
162
+
163
+ public render(): TemplateResult {
164
+ const renderers: Record<TIdpSecureAccountStage, () => { heading: string; description: string; body: TemplateResult }> = {
165
+ choice: () => ({
166
+ heading: 'Secure your account',
167
+ description: 'Add a second factor now. You can also set this up later under Security in your console.',
168
+ body: html`
169
+ <div class="option-list">
170
+ <idp-option-row
171
+ tileTone="accent"
172
+ tileSize="34"
173
+ icon="fingerprint"
174
+ label="Add a passkey"
175
+ description="Sign in with Face ID, Touch ID, or a security key"
176
+ trailing="chevron"
177
+ @click=${() => { if (!this.busy) this.emit('idp-secure-passkey'); }}
178
+ ></idp-option-row>
179
+ <idp-option-row
180
+ tileTone="accent"
181
+ tileSize="34"
182
+ icon="lucide:Smartphone"
183
+ label="Use an authenticator app"
184
+ description="Time-based codes from an app like 1Password or Google Authenticator"
185
+ trailing="chevron"
186
+ @click=${() => { if (!this.busy) this.emit('idp-secure-totp'); }}
187
+ ></idp-option-row>
188
+ </div>
189
+ ${this.errorText ? html`<div class="error-note" role="alert">${this.errorText}</div>` : html``}
190
+ <div class="skip-row"><a @click=${() => this.emit('idp-secure-skip')}>Skip for now</a></div>
191
+ `,
192
+ }),
193
+ totp: () => ({
194
+ heading: 'Set up your authenticator',
195
+ description: 'Add the secret to your authenticator app, then enter the current 6-digit code.',
196
+ body: html`
197
+ <div class="totp-block">
198
+ <div>
199
+ <div class="code-label">Manual secret</div>
200
+ <div class="code-block">${this.totpSecret}</div>
201
+ </div>
202
+ ${this.totpOtpauthUrl
203
+ ? html`
204
+ <div>
205
+ <div class="code-label">Or open directly on this device</div>
206
+ <div class="code-block"><a href=${this.totpOtpauthUrl}>${this.totpOtpauthUrl}</a></div>
207
+ </div>
208
+ `
209
+ : html``}
210
+ <div class="otp-row">
211
+ <idp-otp-input
212
+ @idp-otp-complete=${(eventArg: CustomEvent<IIdpOtpChangeEventDetail>) => {
213
+ this.dispatchEvent(new CustomEvent('idp-secure-totp-verify', {
214
+ detail: { code: eventArg.detail.value },
215
+ bubbles: true,
216
+ composed: true,
217
+ }));
218
+ }}
219
+ ></idp-otp-input>
220
+ </div>
221
+ </div>
222
+ ${this.errorText ? html`<div class="error-note" role="alert">${this.errorText}</div>` : html``}
223
+ <div class="skip-row"><a @click=${() => this.emit('idp-secure-back')}>Choose a different method</a></div>
224
+ `,
225
+ }),
226
+ 'backup-codes': () => ({
227
+ heading: 'Save your backup codes',
228
+ description: 'Each code works once if you lose access to your authenticator. Store them somewhere safe.',
229
+ body: html`
230
+ <div class="backup-grid">
231
+ ${this.backupCodes.map((codeArg) => html`<div class="backup-code">${codeArg}</div>`)}
232
+ </div>
233
+ <div class="actions">
234
+ <idp-button variant="accent" shape="rect" block @click=${() => this.emit('idp-secure-continue')}>I saved these codes</idp-button>
235
+ </div>
236
+ `,
237
+ }),
238
+ done: () => ({
239
+ heading: this.doneMethod === 'totp' ? 'Authenticator added' : 'Passkey added',
240
+ description: '',
241
+ body: html`
242
+ <div class="status-block">
243
+ <idp-status-icon tone="ok" icon="check"></idp-status-icon>
244
+ <div class="status-text">
245
+ ${this.doneMethod === 'totp'
246
+ ? 'Sign-ins now require a code from your authenticator app.'
247
+ : 'You can now sign in with your passkey instead of a password.'}
248
+ </div>
249
+ </div>
250
+ <div class="actions">
251
+ <idp-button variant="accent" shape="rect" block @click=${() => this.emit('idp-secure-continue')}>Continue</idp-button>
252
+ </div>
253
+ `,
254
+ }),
255
+ };
256
+ const view = renderers[this.stage]();
257
+ return html`
258
+ <idp-auth-card heading=${view.heading} description=${view.description}>
259
+ ${view.body}
260
+ </idp-auth-card>
261
+ `;
262
+ }
263
+
264
+ private emit(eventName: string) {
265
+ this.dispatchEvent(new CustomEvent(eventName, { bubbles: true, composed: true }));
266
+ }
267
+ }
@@ -23,6 +23,7 @@ export * from './idp-app-pair.js';
23
23
  export * from './idp-auth-card.js';
24
24
  export * from './idp-consent-card.js';
25
25
  export * from './idp-invite-card.js';
26
+ export * from './idp-secure-account-card.js';
26
27
  export * from './idp-approval-card.js';
27
28
  export * from './idp-mobile-frame.js';
28
29
  export * from './idp-inbox-preview.js';