@atlasauth/angular 0.3.0 → 0.5.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.
- package/CHANGELOG.md +53 -0
- package/dist/esm2022/lib/atlas.service.mjs +115 -33
- package/dist/esm2022/lib/components/attempt-flow.component.mjs +76 -9
- package/dist/esm2022/lib/components/flow-controller.mjs +49 -2
- package/dist/esm2022/lib/components/sign-in.component.mjs +11 -4
- package/dist/esm2022/lib/components/sign-up-form.component.mjs +44 -15
- package/dist/esm2022/lib/components/sign-up.component.mjs +2 -2
- package/dist/esm2022/lib/i18n.mjs +44 -1
- package/dist/esm2022/public-api.mjs +4 -1
- package/dist/fesm2022/atlasauth-angular.mjs +332 -58
- package/dist/fesm2022/atlasauth-angular.mjs.map +1 -1
- package/dist/lib/atlas.service.d.ts +37 -3
- package/dist/lib/components/attempt-flow.component.d.ts +16 -1
- package/dist/lib/components/flow-controller.d.ts +26 -0
- package/dist/lib/components/sign-up-form.component.d.ts +8 -2
- package/dist/public-api.d.ts +2 -1
- package/package.json +5 -4
|
@@ -4,8 +4,8 @@ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
|
4
4
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
5
5
|
import { BehaviorSubject, combineLatest, firstValueFrom } from 'rxjs';
|
|
6
6
|
import { map, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
|
7
|
-
import { TokenCache, resolveFrontendApi, FapiClient,
|
|
8
|
-
export { FapiClient, advance, flowFromPending, getProviderToken, initialFlow, pollAttempt, prepareFactor, requestForStep, shouldKeepPolling } from '@atlasauth/js';
|
|
7
|
+
import { TokenCache, resolveFrontendApi, catalogLocale, FapiClient, SignOutGuard, localizedErrorMessage, getProviderToken, readRedirectResult, completeOAuthPopupLanding, mapOAuthPopupPayload, stripRedirectParams, closeOAuthPopupAfterSignIn, readExpiry, msUntilRefresh, nextStep, createCaptchaController, flowFromPending, initialFlow, fetchInstanceSettings, offeredFirstFactors, prepareFactor, signInCaptchaOptions, flowNeedsCaptcha, advanceWithCaptcha, createSignUp, shouldKeepPolling, pollAttempt } from '@atlasauth/js';
|
|
8
|
+
export { FapiClient, SignOutError, advance, flowFromPending, getProviderToken, initialFlow, pollAttempt, prepareFactor, requestForStep, shouldKeepPolling } from '@atlasauth/js';
|
|
9
9
|
import { evaluate } from '@atlasauth/authz';
|
|
10
10
|
export { evaluate, hasFromClaims } from '@atlasauth/authz';
|
|
11
11
|
import { renderDesignAppearance, instanceDesignFrom, applyAppearanceToDom } from '@atlasauth/design/runtime';
|
|
@@ -167,6 +167,7 @@ const EN_US = {
|
|
|
167
167
|
'signIn.orDivider': 'or',
|
|
168
168
|
'signIn.socialButton': 'Continue with {provider}',
|
|
169
169
|
'signIn.emailCodeLabel': 'Verification code',
|
|
170
|
+
'signIn.useEmailCode': 'Email me a code',
|
|
170
171
|
'signIn.magicLinkSent': 'Check your email for a sign-in link.',
|
|
171
172
|
'signIn.checkOtherTab': 'You can close this tab — you are signed in where you started.',
|
|
172
173
|
'signUp.title': 'Create your account',
|
|
@@ -182,6 +183,8 @@ const EN_US = {
|
|
|
182
183
|
'signUp.terms': 'Terms of Service',
|
|
183
184
|
'signUp.privacy': 'Privacy Policy',
|
|
184
185
|
'signUp.and': 'and',
|
|
186
|
+
// = EMAIL_UNAVAILABLE_MESSAGE from @atlasauth/js.
|
|
187
|
+
'signUp.emailUnavailable': "This app can't send email right now. Try another sign-in method or contact the app owner.",
|
|
185
188
|
'captcha.prompt': 'Complete the security check to continue.',
|
|
186
189
|
'captcha.required': 'Please complete the security check, then try again.',
|
|
187
190
|
'captcha.failed': "The security check didn't pass. Please try again.",
|
|
@@ -217,6 +220,46 @@ const EN_US = {
|
|
|
217
220
|
'organization.roleLabel': 'Role',
|
|
218
221
|
'error.generic': 'Something went wrong. Please try again.',
|
|
219
222
|
'error.network': 'We could not reach the server. Check your connection.',
|
|
223
|
+
// Server errors, by the stable `reason` the Frontend API sends. Shown instead
|
|
224
|
+
// of the server's `message` (see `localizedErrorMessage` in @atlasauth/js).
|
|
225
|
+
'error.form_password_incorrect': 'Those credentials are incorrect.',
|
|
226
|
+
'error.form_code_incorrect': 'That code is incorrect.',
|
|
227
|
+
'error.form_code_already_used': 'That code has already been used. Wait for a new one.',
|
|
228
|
+
'error.form_code_expired': 'That code has expired. Request a new one.',
|
|
229
|
+
'error.form_code_max_attempts': 'Too many incorrect codes. Request a new code.',
|
|
230
|
+
'error.form_link_invalid': 'That sign-in link is invalid or has already been used.',
|
|
231
|
+
'error.form_identifier_not_found': "We couldn't find an account with those details.",
|
|
232
|
+
'error.form_identifier_exists': "There's already an account for this email. Sign in with {methods}.",
|
|
233
|
+
'error.form_password_pwned': 'This password has appeared in a known data breach. Please choose a different one.',
|
|
234
|
+
'error.form_password_too_short': 'Password must be at least {min_length} characters.',
|
|
235
|
+
'error.form_password_too_long': 'Password must be at most {max_length} characters.',
|
|
236
|
+
'error.form_password_requires_uppercase': 'Password must contain an uppercase letter.',
|
|
237
|
+
'error.form_password_requires_lowercase': 'Password must contain a lowercase letter.',
|
|
238
|
+
'error.form_password_requires_number': 'Password must contain a number.',
|
|
239
|
+
'error.form_password_requires_symbol': 'Password must contain a symbol.',
|
|
240
|
+
'error.form_password_not_allowed': 'That password is not allowed. Choose a different one.',
|
|
241
|
+
'error.form_password_contains_identifier': 'Password must not contain your email or username.',
|
|
242
|
+
'error.form_password_same_as_current': 'Your new password must be different from your current one.',
|
|
243
|
+
'error.form_password_reused': 'Your new password must not match any of your last {history_count} passwords.',
|
|
244
|
+
'error.password_expired': 'Your password has expired. Reset it to continue.',
|
|
245
|
+
'error.email_not_verified': 'Your email address is not verified yet. Verify it before signing in with your password.',
|
|
246
|
+
'error.sign_in_token_invalid': 'That sign-in token is invalid or has expired.',
|
|
247
|
+
'error.too_many_attempts': 'Too many failed attempts. Try again later.',
|
|
248
|
+
'error.too_many_requests': 'Too many requests. Please try again later.',
|
|
249
|
+
'error.captcha_required': 'Please complete the security check, then try again.',
|
|
250
|
+
'error.captcha_failed': "The security check didn't pass. Please try again.",
|
|
251
|
+
'error.captcha_unavailable': 'The security check is unavailable right now. Please try again later.',
|
|
252
|
+
'error.email_delivery_unavailable': "This app can't send email right now. Try another sign-in method or contact the app owner.",
|
|
253
|
+
'error.strategy_disabled': 'This sign-in method is not available for this app.',
|
|
254
|
+
'error.sign_up_restricted': 'Sign-up on this app is by invitation only.',
|
|
255
|
+
'error.waitlisted': "You're on the waitlist. We'll email you when a place opens up.",
|
|
256
|
+
'error.identifier_not_allowed': 'This email or phone number is not allowed on this app.',
|
|
257
|
+
'error.disposable_email_blocked': 'Sign-up with a disposable or temporary email address is not allowed.',
|
|
258
|
+
'error.sso_required': 'This organization requires signing in through single sign-on.',
|
|
259
|
+
'error.sign_ins_disabled': 'Sign-ins are temporarily disabled.',
|
|
260
|
+
'error.sign_ups_disabled': 'Sign-ups are temporarily disabled.',
|
|
261
|
+
'error.high_risk_blocked': 'This sign-in was blocked because it looked unusual.',
|
|
262
|
+
'error.sms_region_blocked': 'Text messages are not available for this phone number.',
|
|
220
263
|
};
|
|
221
264
|
/**
|
|
222
265
|
* Look up a key and interpolate `{placeholders}`.
|
|
@@ -262,7 +305,19 @@ class AtlasService {
|
|
|
262
305
|
cache = new TokenCache();
|
|
263
306
|
fapiClient;
|
|
264
307
|
refreshTimer = null;
|
|
308
|
+
/**
|
|
309
|
+
* The sessions this browser signed out of (tombstones in localStorage, per
|
|
310
|
+
* Frontend API origin), the revoke and its background re-tries. Every reload /
|
|
311
|
+
* refresh asks it before adopting a session, so a sign-out the server never
|
|
312
|
+
* confirmed is not undone by the still-valid refresh cookie.
|
|
313
|
+
*/
|
|
314
|
+
guard;
|
|
315
|
+
stopWatchingSignOut = null;
|
|
265
316
|
catalog;
|
|
317
|
+
/** The language layer of `catalog` (the `localization` bundle alone; `EN_US` when none). */
|
|
318
|
+
errorCatalog;
|
|
319
|
+
/** Sent to the Frontend API as `Accept-Language`: the `localization` bundle's `locale` tag. */
|
|
320
|
+
requestLocale;
|
|
266
321
|
/**
|
|
267
322
|
* A sign-in attempt handed back by an OAuth redirect that still needs a second
|
|
268
323
|
* factor (no session was issued). Null in the ordinary case; the `<atlas-sign-in>`
|
|
@@ -318,10 +373,17 @@ class AtlasService {
|
|
|
318
373
|
});
|
|
319
374
|
this.doFetch = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
320
375
|
this.catalog = options.localization ? withFallback(options.localization) : EN_US;
|
|
376
|
+
this.errorCatalog = options.localization ?? EN_US;
|
|
377
|
+
this.requestLocale = catalogLocale(options.localization);
|
|
321
378
|
this.fapiClient = new FapiClient({
|
|
322
379
|
publishableKey: this.publishableKey,
|
|
323
380
|
baseUrl: this.baseUrl,
|
|
324
381
|
fetchImpl: this.doFetch,
|
|
382
|
+
locale: this.requestLocale,
|
|
383
|
+
});
|
|
384
|
+
this.guard = new SignOutGuard({
|
|
385
|
+
request: (path, init) => this.call(path, init),
|
|
386
|
+
frontendApi: this.baseUrl,
|
|
325
387
|
});
|
|
326
388
|
// Captured synchronously (not in an async init) so it is available before any
|
|
327
389
|
// flow component reads it: a needs_second_factor redirect carries a status but
|
|
@@ -330,10 +392,19 @@ class AtlasService {
|
|
|
330
392
|
// Boot once, browser only. On the server there is no cookie jar to read a
|
|
331
393
|
// session from, so we leave the state at `loading` and let hydration boot it.
|
|
332
394
|
if (this.isBrowser) {
|
|
395
|
+
// Another tab signed out of THIS session (its tombstone reached the shared
|
|
396
|
+
// store): drop the token and re-check, which then refuses the session.
|
|
397
|
+
this.stopWatchingSignOut = this.guard.watch(() => this.cache.peek()?.sessionId ?? this._session.value?.id, () => {
|
|
398
|
+
this.cache.clear();
|
|
399
|
+
void this.reload();
|
|
400
|
+
});
|
|
333
401
|
void this.boot();
|
|
334
402
|
}
|
|
335
403
|
}
|
|
336
404
|
ngOnDestroy() {
|
|
405
|
+
this.stopWatchingSignOut?.();
|
|
406
|
+
this.stopWatchingSignOut = null;
|
|
407
|
+
this.guard.dispose();
|
|
337
408
|
this.clearRefreshTimer();
|
|
338
409
|
this._status.complete();
|
|
339
410
|
this._user.complete();
|
|
@@ -351,6 +422,22 @@ class AtlasService {
|
|
|
351
422
|
get frontendApi() {
|
|
352
423
|
return this.baseUrl;
|
|
353
424
|
}
|
|
425
|
+
/** The `Accept-Language` sent to the Frontend API: the `localization` bundle's `locale` tag, if any. */
|
|
426
|
+
get locale() {
|
|
427
|
+
return this.requestLocale;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The text to show for a server error, in the app's language: the catalog's
|
|
431
|
+
* `error.<reason>` (looked up in the `localization` bundle, or `EN_US` when
|
|
432
|
+
* none is set), else the server's message, else `error.generic`.
|
|
433
|
+
*/
|
|
434
|
+
errorText(error) {
|
|
435
|
+
return localizedErrorMessage(error, this.errorCatalog, this.catalog);
|
|
436
|
+
}
|
|
437
|
+
/** The catalog server errors are looked up in (see {@link errorText}). */
|
|
438
|
+
get errorLocalization() {
|
|
439
|
+
return this.errorCatalog;
|
|
440
|
+
}
|
|
354
441
|
/** Tokens as CSS custom properties, for a host element to inherit. */
|
|
355
442
|
appearanceVariables() {
|
|
356
443
|
return cssVariables(resolveTokens(this.options.appearance));
|
|
@@ -384,7 +471,11 @@ class AtlasService {
|
|
|
384
471
|
}
|
|
385
472
|
/** A fresh {@link FapiClient} for driving a sign-in/sign-up flow. */
|
|
386
473
|
createFapiClient() {
|
|
387
|
-
return new FapiClient({
|
|
474
|
+
return new FapiClient({
|
|
475
|
+
publishableKey: this.publishableKey,
|
|
476
|
+
baseUrl: this.baseUrl,
|
|
477
|
+
locale: this.requestLocale,
|
|
478
|
+
});
|
|
388
479
|
}
|
|
389
480
|
// --- Imperative API (mirrors the React hooks' actions) ----------------------
|
|
390
481
|
/**
|
|
@@ -412,25 +503,27 @@ class AtlasService {
|
|
|
412
503
|
has(condition) {
|
|
413
504
|
return evaluate(this._claims.value, condition).allowed;
|
|
414
505
|
}
|
|
415
|
-
/**
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
this.
|
|
431
|
-
this.
|
|
432
|
-
this.
|
|
433
|
-
this.
|
|
506
|
+
/**
|
|
507
|
+
* Revoke the current session server-side and drop all local auth state.
|
|
508
|
+
*
|
|
509
|
+
* The session id is remembered before the revoke (a tombstone shared by every
|
|
510
|
+
* tab), so a reload, a token refresh, a sibling tab or a tab opened later never
|
|
511
|
+
* re-adopts it — even when the revoke failed (it is retried once, then again in
|
|
512
|
+
* the background) and the refresh cookie is still valid. Resolves
|
|
513
|
+
* `{ revoked, activeSessionId? }`; with `{ strict: true }` an unconfirmed revoke
|
|
514
|
+
* rejects with a `SignOutError` instead (after the local sign-out). A parked
|
|
515
|
+
* session the server promoted on revoke is adopted. Front-channel logout runs
|
|
516
|
+
* unless `{ frontchannel: false }`.
|
|
517
|
+
*/
|
|
518
|
+
async signOut(options = {}) {
|
|
519
|
+
// With no cached token the guard finds the cookie session itself
|
|
520
|
+
// (GET /v1/client), so a missing JWT never skips the revoke.
|
|
521
|
+
const sessionId = this.cache.peek()?.sessionId ?? this._session.value?.id ?? null;
|
|
522
|
+
const attempt = await this.guard.revoke(sessionId, options);
|
|
523
|
+
this.clearLocalState();
|
|
524
|
+
if (this.guard.promoted(attempt))
|
|
525
|
+
await this.reload();
|
|
526
|
+
return this.guard.settle(attempt, options);
|
|
434
527
|
}
|
|
435
528
|
/**
|
|
436
529
|
* Switch the active organization (or `null` for the personal account). The
|
|
@@ -458,14 +551,10 @@ class AtlasService {
|
|
|
458
551
|
if (!response.ok)
|
|
459
552
|
throw new Error('boot failed');
|
|
460
553
|
const body = (await response.json());
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
this.
|
|
465
|
-
this._user.next(null);
|
|
466
|
-
this._session.next(null);
|
|
467
|
-
this._claims.next(null);
|
|
468
|
-
this._memberships.next([]);
|
|
554
|
+
// A session this browser signed out of is never re-adopted from the
|
|
555
|
+
// refresh cookie; the guard re-tries its revoke in the background.
|
|
556
|
+
if (!body.session || !body.user || this.guard.refuse(body.session.id)) {
|
|
557
|
+
this.clearLocalState();
|
|
469
558
|
return;
|
|
470
559
|
}
|
|
471
560
|
this._user.next(body.user);
|
|
@@ -493,17 +582,41 @@ class AtlasService {
|
|
|
493
582
|
* has no ticket, so nothing is exchanged and the app boots signed-out.
|
|
494
583
|
*/
|
|
495
584
|
async boot() {
|
|
496
|
-
const
|
|
585
|
+
const fromUrl = readRedirectResult(window.location.search);
|
|
586
|
+
let result = fromUrl;
|
|
587
|
+
// §oauth popup mode: this page may be the landing of a sign-in popup whose
|
|
588
|
+
// opener a Cross-Origin-Opener-Policy severed (`#atlas_popup_result=…`).
|
|
589
|
+
// The result is relayed to the opener, which redeems the ticket, and this
|
|
590
|
+
// window closes. If no opener takes it (its tab is gone), finish here like
|
|
591
|
+
// a redirect.
|
|
592
|
+
const landing = await completeOAuthPopupLanding().catch(() => null);
|
|
593
|
+
if (landing && !landing.delivered) {
|
|
594
|
+
const outcome = mapOAuthPopupPayload(landing.payload);
|
|
595
|
+
if (outcome?.ticket && outcome.attemptId) {
|
|
596
|
+
result = { attemptId: outcome.attemptId, ticket: outcome.ticket };
|
|
597
|
+
}
|
|
598
|
+
else if (outcome?.continuationUrl) {
|
|
599
|
+
// A second factor / connect step / provider error: the redirect flow's page.
|
|
600
|
+
window.location.replace(outcome.continuationUrl);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
let exchanged = false;
|
|
497
605
|
if (result?.ticket) {
|
|
498
|
-
await this.call('/v1/client/tickets/exchange', {
|
|
606
|
+
const response = await this.call('/v1/client/tickets/exchange', {
|
|
499
607
|
method: 'POST',
|
|
500
608
|
body: JSON.stringify({ attempt_id: result.attemptId, ticket: result.ticket }),
|
|
501
609
|
}).catch(() => undefined);
|
|
610
|
+
exchanged = !!response?.ok;
|
|
502
611
|
}
|
|
503
|
-
if (
|
|
612
|
+
if (fromUrl) {
|
|
504
613
|
window.history.replaceState({}, document.title, stripRedirectParams(window.location.href));
|
|
505
614
|
}
|
|
506
615
|
await this.reload();
|
|
616
|
+
// Signed in inside the Atlas popup (the redirect flow ran in it): hand back
|
|
617
|
+
// to the waiting opener and close. A no-op in an ordinary tab.
|
|
618
|
+
if (exchanged)
|
|
619
|
+
void closeOAuthPopupAfterSignIn();
|
|
507
620
|
}
|
|
508
621
|
readPendingAttempt() {
|
|
509
622
|
const result = readRedirectResult(window.location.search);
|
|
@@ -525,6 +638,9 @@ class AtlasService {
|
|
|
525
638
|
});
|
|
526
639
|
}
|
|
527
640
|
applyToken(jwt, sessionId) {
|
|
641
|
+
// An in-flight refresh must not resurrect a session signed out meanwhile.
|
|
642
|
+
if (this.guard.isSignedOut(sessionId))
|
|
643
|
+
return;
|
|
528
644
|
const expiresAt = readExpiry(jwt);
|
|
529
645
|
if (!expiresAt)
|
|
530
646
|
return;
|
|
@@ -550,6 +666,15 @@ class AtlasService {
|
|
|
550
666
|
await this.reload();
|
|
551
667
|
}
|
|
552
668
|
}
|
|
669
|
+
clearLocalState() {
|
|
670
|
+
this.clearRefreshTimer();
|
|
671
|
+
this.cache.clear();
|
|
672
|
+
this._status.next('signed_out');
|
|
673
|
+
this._user.next(null);
|
|
674
|
+
this._session.next(null);
|
|
675
|
+
this._claims.next(null);
|
|
676
|
+
this._memberships.next([]);
|
|
677
|
+
}
|
|
553
678
|
/** §7.4: proactive, jittered refresh scheduled from the token's own expiry. */
|
|
554
679
|
scheduleRefresh() {
|
|
555
680
|
this.clearRefreshTimer();
|
|
@@ -901,12 +1026,28 @@ class AttemptFlowComponent {
|
|
|
901
1026
|
* with `needs_captcha`. Null when the flow draws none.
|
|
902
1027
|
*/
|
|
903
1028
|
captcha = null;
|
|
1029
|
+
/**
|
|
1030
|
+
* The first factors to offer on the `needs_first_factor` step — pass
|
|
1031
|
+
* `AtlasFlowController.firstFactors()`, which drops email code / link while the
|
|
1032
|
+
* app cannot deliver email. Null uses the server's list as-is.
|
|
1033
|
+
*/
|
|
1034
|
+
firstFactors = null;
|
|
1035
|
+
/** The first factor already sent (`AtlasFlowController.preparedFactor()`): its code is collected. */
|
|
1036
|
+
preparedFactor = null;
|
|
904
1037
|
/** Emits the collected field values when the form is submitted. */
|
|
905
1038
|
submitFlow = new EventEmitter();
|
|
1039
|
+
/** Emits when the user picks a first factor the server must send first (an emailed code). */
|
|
1040
|
+
chooseFactor = new EventEmitter();
|
|
906
1041
|
values = {};
|
|
907
1042
|
t(key, vars) {
|
|
908
1043
|
return translate(this.atlas.localization, key, vars);
|
|
909
1044
|
}
|
|
1045
|
+
/** A server error in the app's language (`error.<reason>`, else the server's message). */
|
|
1046
|
+
errorText(error) {
|
|
1047
|
+
// `errorLocalization` is read defensively: a stand-in service may lack it.
|
|
1048
|
+
const layer = this.atlas.errorLocalization;
|
|
1049
|
+
return localizedErrorMessage(error, layer ?? this.atlas.localization, this.atlas.localization);
|
|
1050
|
+
}
|
|
910
1051
|
cls(key, base) {
|
|
911
1052
|
return classFor(key, this.atlas.appearance, base);
|
|
912
1053
|
}
|
|
@@ -930,8 +1071,22 @@ class AttemptFlowComponent {
|
|
|
930
1071
|
const kind = this.step().kind;
|
|
931
1072
|
return !!this.captcha && (kind === 'collect_identifier' || kind === 'collect_captcha');
|
|
932
1073
|
}
|
|
1074
|
+
/** The first factors on offer at a `needs_first_factor` step (empty on any other). */
|
|
1075
|
+
offered() {
|
|
1076
|
+
const step = this.step();
|
|
1077
|
+
if (step.kind !== 'collect_first_factor')
|
|
1078
|
+
return [];
|
|
1079
|
+
return this.firstFactors ?? step.strategies;
|
|
1080
|
+
}
|
|
1081
|
+
showEmailCodeOption() {
|
|
1082
|
+
return this.preparedFactor !== 'email_code' && this.offered().includes('email_code');
|
|
1083
|
+
}
|
|
933
1084
|
showSubmit() {
|
|
934
1085
|
const kind = this.step().kind;
|
|
1086
|
+
// Nothing to submit on a first-factor step that only offers an emailed code
|
|
1087
|
+
// until that code is sent.
|
|
1088
|
+
if (kind === 'collect_first_factor')
|
|
1089
|
+
return this.fields().length > 0;
|
|
935
1090
|
return kind !== 'done' && kind !== 'await_oauth';
|
|
936
1091
|
}
|
|
937
1092
|
fields() {
|
|
@@ -939,8 +1094,17 @@ class AttemptFlowComponent {
|
|
|
939
1094
|
switch (step.kind) {
|
|
940
1095
|
case 'collect_identifier':
|
|
941
1096
|
return [{ kind: 'input', name: 'identifier', labelKey: 'signIn.identifierLabel', type: 'email' }];
|
|
942
|
-
case 'collect_first_factor':
|
|
943
|
-
|
|
1097
|
+
case 'collect_first_factor': {
|
|
1098
|
+
if (this.preparedFactor === 'email_code') {
|
|
1099
|
+
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
1100
|
+
}
|
|
1101
|
+
// The password field unless the only offer is an emailed code (chosen
|
|
1102
|
+
// with the button below the form).
|
|
1103
|
+
const offered = this.offered();
|
|
1104
|
+
return offered.includes('email_code') && !offered.includes('password')
|
|
1105
|
+
? []
|
|
1106
|
+
: [{ kind: 'input', name: 'password', labelKey: 'signIn.passwordLabel', type: 'password' }];
|
|
1107
|
+
}
|
|
944
1108
|
case 'collect_second_factor':
|
|
945
1109
|
return [{ kind: 'input', name: 'code', labelKey: 'mfa.codeLabel', type: 'text' }];
|
|
946
1110
|
case 'enroll_second_factor':
|
|
@@ -971,12 +1135,12 @@ class AttemptFlowComponent {
|
|
|
971
1135
|
}
|
|
972
1136
|
}
|
|
973
1137
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
974
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: AttemptFlowComponent, isStandalone: true, selector: "atlas-attempt-flow", inputs: { attempt: "attempt", busy: "busy", errors: "errors", titleKey: "titleKey", captcha: "captcha" }, outputs: { submitFlow: "submitFlow" }, ngImport: i0, template: `
|
|
1138
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: AttemptFlowComponent, isStandalone: true, selector: "atlas-attempt-flow", inputs: { attempt: "attempt", busy: "busy", errors: "errors", titleKey: "titleKey", captcha: "captcha", firstFactors: "firstFactors", preparedFactor: "preparedFactor" }, outputs: { submitFlow: "submitFlow", chooseFactor: "chooseFactor" }, ngImport: i0, template: `
|
|
975
1139
|
<div [class]="cls('card', 'atlas-card')">
|
|
976
1140
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
977
1141
|
|
|
978
1142
|
@for (error of formErrors(); track error.code) {
|
|
979
|
-
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error
|
|
1143
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ errorText(error) }}</p>
|
|
980
1144
|
}
|
|
981
1145
|
|
|
982
1146
|
<form (submit)="onSubmit($event)">
|
|
@@ -992,7 +1156,7 @@ class AttemptFlowComponent {
|
|
|
992
1156
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
993
1157
|
/>
|
|
994
1158
|
@for (error of fieldErrorsFor(f.name); track error.code) {
|
|
995
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1159
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
996
1160
|
}
|
|
997
1161
|
</label>
|
|
998
1162
|
} @else {
|
|
@@ -1009,6 +1173,17 @@ class AttemptFlowComponent {
|
|
|
1009
1173
|
{{ t('signIn.submit') }}
|
|
1010
1174
|
</button>
|
|
1011
1175
|
}
|
|
1176
|
+
|
|
1177
|
+
@if (showEmailCodeOption()) {
|
|
1178
|
+
<button
|
|
1179
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1180
|
+
[disabled]="busy"
|
|
1181
|
+
type="button"
|
|
1182
|
+
(click)="chooseFactor.emit('email_code')"
|
|
1183
|
+
>
|
|
1184
|
+
{{ t('signIn.useEmailCode') }}
|
|
1185
|
+
</button>
|
|
1186
|
+
}
|
|
1012
1187
|
</form>
|
|
1013
1188
|
</div>
|
|
1014
1189
|
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
@@ -1025,7 +1200,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1025
1200
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
1026
1201
|
|
|
1027
1202
|
@for (error of formErrors(); track error.code) {
|
|
1028
|
-
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error
|
|
1203
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ errorText(error) }}</p>
|
|
1029
1204
|
}
|
|
1030
1205
|
|
|
1031
1206
|
<form (submit)="onSubmit($event)">
|
|
@@ -1041,7 +1216,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1041
1216
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
1042
1217
|
/>
|
|
1043
1218
|
@for (error of fieldErrorsFor(f.name); track error.code) {
|
|
1044
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1219
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1045
1220
|
}
|
|
1046
1221
|
</label>
|
|
1047
1222
|
} @else {
|
|
@@ -1058,6 +1233,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1058
1233
|
{{ t('signIn.submit') }}
|
|
1059
1234
|
</button>
|
|
1060
1235
|
}
|
|
1236
|
+
|
|
1237
|
+
@if (showEmailCodeOption()) {
|
|
1238
|
+
<button
|
|
1239
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1240
|
+
[disabled]="busy"
|
|
1241
|
+
type="button"
|
|
1242
|
+
(click)="chooseFactor.emit('email_code')"
|
|
1243
|
+
>
|
|
1244
|
+
{{ t('signIn.useEmailCode') }}
|
|
1245
|
+
</button>
|
|
1246
|
+
}
|
|
1061
1247
|
</form>
|
|
1062
1248
|
</div>
|
|
1063
1249
|
`,
|
|
@@ -1072,8 +1258,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1072
1258
|
type: Input
|
|
1073
1259
|
}], captcha: [{
|
|
1074
1260
|
type: Input
|
|
1261
|
+
}], firstFactors: [{
|
|
1262
|
+
type: Input
|
|
1263
|
+
}], preparedFactor: [{
|
|
1264
|
+
type: Input
|
|
1075
1265
|
}], submitFlow: [{
|
|
1076
1266
|
type: Output
|
|
1267
|
+
}], chooseFactor: [{
|
|
1268
|
+
type: Output
|
|
1077
1269
|
}] } });
|
|
1078
1270
|
/** A read-only message row: `labelKey` holds the i18n key of the text to show. */
|
|
1079
1271
|
function msg(labelKey) {
|
|
@@ -1113,6 +1305,12 @@ class AtlasFlowController {
|
|
|
1113
1305
|
captchaDrawn = signal(false);
|
|
1114
1306
|
/** Whether {@link signUpCaptcha} currently draws a widget (for templates). */
|
|
1115
1307
|
signUpCaptchaDrawn = signal(false);
|
|
1308
|
+
/**
|
|
1309
|
+
* The first factor the server was asked to send ({@link prepare}) on the
|
|
1310
|
+
* current `needs_first_factor` step — the form then collects its code. Null
|
|
1311
|
+
* otherwise.
|
|
1312
|
+
*/
|
|
1313
|
+
preparedFactor = signal(null);
|
|
1116
1314
|
pollTimer = null;
|
|
1117
1315
|
identifier = '';
|
|
1118
1316
|
constructor(client, reload, pending,
|
|
@@ -1134,6 +1332,44 @@ class AtlasFlowController {
|
|
|
1134
1332
|
this.syncCaptcha();
|
|
1135
1333
|
}
|
|
1136
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* The first factors to offer on a `needs_first_factor` step: the server's list
|
|
1337
|
+
* without email code / link while the app cannot deliver email
|
|
1338
|
+
* (`settings.emailDelivery.available === false`). Empty on any other step.
|
|
1339
|
+
*/
|
|
1340
|
+
firstFactors() {
|
|
1341
|
+
const attempt = this.state().attempt;
|
|
1342
|
+
const step = attempt ? nextStep(attempt) : null;
|
|
1343
|
+
return step?.kind === 'collect_first_factor'
|
|
1344
|
+
? offeredFirstFactors(step.strategies, this.settings()?.emailDelivery)
|
|
1345
|
+
: [];
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* True when email + password sign-up cannot complete because the app cannot
|
|
1349
|
+
* send the verification email (`settings.emailDelivery.passwordSignUp === false`):
|
|
1350
|
+
* `<atlas-sign-up>` shows the "can't send email" notice instead of the form.
|
|
1351
|
+
*/
|
|
1352
|
+
signUpEmailUnavailable() {
|
|
1353
|
+
return this.settings()?.emailDelivery.passwordSignUp === false;
|
|
1354
|
+
}
|
|
1355
|
+
/** False when a password reset email cannot be delivered — hide "Forgot password?". */
|
|
1356
|
+
passwordResetAvailable() {
|
|
1357
|
+
return this.settings()?.emailDelivery.passwordReset !== false;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Ask the server to email a sign-in code for the current `needs_first_factor`
|
|
1361
|
+
* step (`POST …/prepare_first_factor`); the form then collects the code, which
|
|
1362
|
+
* {@link submit} posts as the `email_code` strategy.
|
|
1363
|
+
*/
|
|
1364
|
+
async prepare(strategy) {
|
|
1365
|
+
if (!this.firstFactors().includes(strategy))
|
|
1366
|
+
return;
|
|
1367
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1368
|
+
const next = await prepareFactor(this.client, this.state(), strategy);
|
|
1369
|
+
this.setState(next);
|
|
1370
|
+
if (!next.errors.length)
|
|
1371
|
+
this.preparedFactor.set(strategy);
|
|
1372
|
+
}
|
|
1137
1373
|
/** Keep both captchas in step with the settings and the flow's current step. */
|
|
1138
1374
|
syncCaptcha() {
|
|
1139
1375
|
const settings = this.settings();
|
|
@@ -1149,6 +1385,9 @@ class AtlasFlowController {
|
|
|
1149
1385
|
}
|
|
1150
1386
|
setState(next) {
|
|
1151
1387
|
this.state.set(next);
|
|
1388
|
+
// A prepared factor belongs to the first-factor step it was sent for.
|
|
1389
|
+
if (next.attempt?.status !== 'needs_first_factor')
|
|
1390
|
+
this.preparedFactor.set(null);
|
|
1152
1391
|
this.syncCaptcha();
|
|
1153
1392
|
}
|
|
1154
1393
|
/**
|
|
@@ -1261,8 +1500,9 @@ class SignInComponent {
|
|
|
1261
1500
|
ngOnInit() {
|
|
1262
1501
|
// If seeded from a pending attempt that is already pollable, start polling.
|
|
1263
1502
|
this.flow.syncPolling();
|
|
1264
|
-
// §5 the instance's captcha settings
|
|
1265
|
-
// draws it here with no code change
|
|
1503
|
+
// §5 the instance's captcha settings (switching captcha on in the dashboard
|
|
1504
|
+
// draws it here with no code change) and email delivery (no emailed-code
|
|
1505
|
+
// option while the app cannot send email).
|
|
1266
1506
|
if (typeof window !== 'undefined')
|
|
1267
1507
|
void this.flow.loadSettings();
|
|
1268
1508
|
}
|
|
@@ -1277,9 +1517,12 @@ class SignInComponent {
|
|
|
1277
1517
|
[errors]="flow.state().errors"
|
|
1278
1518
|
titleKey="signIn.title"
|
|
1279
1519
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1520
|
+
[firstFactors]="flow.firstFactors()"
|
|
1521
|
+
[preparedFactor]="flow.preparedFactor()"
|
|
1280
1522
|
(submitFlow)="flow.submit($event)"
|
|
1523
|
+
(chooseFactor)="flow.prepare($event)"
|
|
1281
1524
|
></atlas-attempt-flow>
|
|
1282
|
-
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey", "captcha"], outputs: ["submitFlow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1525
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey", "captcha", "firstFactors", "preparedFactor"], outputs: ["submitFlow", "chooseFactor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1283
1526
|
}
|
|
1284
1527
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
|
|
1285
1528
|
type: Component,
|
|
@@ -1295,7 +1538,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1295
1538
|
[errors]="flow.state().errors"
|
|
1296
1539
|
titleKey="signIn.title"
|
|
1297
1540
|
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1541
|
+
[firstFactors]="flow.firstFactors()"
|
|
1542
|
+
[preparedFactor]="flow.preparedFactor()"
|
|
1298
1543
|
(submitFlow)="flow.submit($event)"
|
|
1544
|
+
(chooseFactor)="flow.prepare($event)"
|
|
1299
1545
|
></atlas-attempt-flow>
|
|
1300
1546
|
`,
|
|
1301
1547
|
}]
|
|
@@ -1307,7 +1553,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1307
1553
|
* The create-account step of `<atlas-sign-up>`: the instance's configured extra
|
|
1308
1554
|
* fields, email + password, the legal-consent row and — when the instance
|
|
1309
1555
|
* requires one on sign-up — the captcha widget. Values live on the component so
|
|
1310
|
-
* a refused submit never clears them.
|
|
1556
|
+
* a refused submit never clears them. When the app cannot send the verification
|
|
1557
|
+
* email (`settings.emailDelivery.passwordSignUp === false`) it shows the
|
|
1558
|
+
* `signUp.emailUnavailable` notice instead of the form.
|
|
1311
1559
|
*/
|
|
1312
1560
|
class SignUpFormComponent {
|
|
1313
1561
|
atlas = inject(AtlasService);
|
|
@@ -1322,12 +1570,22 @@ class SignUpFormComponent {
|
|
|
1322
1570
|
t(key, vars) {
|
|
1323
1571
|
return translate(this.atlas.localization, key, vars);
|
|
1324
1572
|
}
|
|
1573
|
+
/** A server error in the app's language (`error.<reason>`, else the server's message). */
|
|
1574
|
+
errorText(error) {
|
|
1575
|
+
// `errorLocalization` is read defensively: a stand-in service may lack it.
|
|
1576
|
+
const layer = this.atlas.errorLocalization;
|
|
1577
|
+
return localizedErrorMessage(error, layer ?? this.atlas.localization, this.atlas.localization);
|
|
1578
|
+
}
|
|
1325
1579
|
cls(key, base) {
|
|
1326
1580
|
return classFor(key, this.atlas.appearance, base);
|
|
1327
1581
|
}
|
|
1328
1582
|
setValue(name, value) {
|
|
1329
1583
|
this.values = { ...this.values, [name]: value };
|
|
1330
1584
|
}
|
|
1585
|
+
/** Email + password sign-up cannot complete: the app cannot send the verification email. */
|
|
1586
|
+
emailUnavailable() {
|
|
1587
|
+
return this.settings?.emailDelivery?.passwordSignUp === false;
|
|
1588
|
+
}
|
|
1331
1589
|
fields() {
|
|
1332
1590
|
return this.settings?.signUpFields ?? [];
|
|
1333
1591
|
}
|
|
@@ -1366,7 +1624,7 @@ class SignUpFormComponent {
|
|
|
1366
1624
|
}
|
|
1367
1625
|
onSubmit(event) {
|
|
1368
1626
|
event.preventDefault();
|
|
1369
|
-
if (!this.consentOk())
|
|
1627
|
+
if (this.emailUnavailable() || !this.consentOk())
|
|
1370
1628
|
return;
|
|
1371
1629
|
this.submitSignUp.emit({
|
|
1372
1630
|
values: { ...this.values },
|
|
@@ -1378,8 +1636,15 @@ class SignUpFormComponent {
|
|
|
1378
1636
|
<div [class]="cls('card', 'atlas-card')">
|
|
1379
1637
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1380
1638
|
|
|
1639
|
+
@if (emailUnavailable()) {
|
|
1640
|
+
<!-- The app cannot send the verification email, so an email + password
|
|
1641
|
+
sign-up could never complete: say so instead of offering the form. -->
|
|
1642
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert" data-atlas-email-unavailable="">
|
|
1643
|
+
{{ t('signUp.emailUnavailable') }}
|
|
1644
|
+
</p>
|
|
1645
|
+
} @else {
|
|
1381
1646
|
@for (error of formErrors(); track error.code) {
|
|
1382
|
-
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error
|
|
1647
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ errorText(error) }}</p>
|
|
1383
1648
|
}
|
|
1384
1649
|
|
|
1385
1650
|
<form (submit)="onSubmit($event)">
|
|
@@ -1402,7 +1667,7 @@ class SignUpFormComponent {
|
|
|
1402
1667
|
}
|
|
1403
1668
|
</span>
|
|
1404
1669
|
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1405
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1670
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1406
1671
|
}
|
|
1407
1672
|
</div>
|
|
1408
1673
|
} @else {
|
|
@@ -1450,7 +1715,7 @@ class SignUpFormComponent {
|
|
|
1450
1715
|
/>
|
|
1451
1716
|
}
|
|
1452
1717
|
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1453
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1718
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1454
1719
|
}
|
|
1455
1720
|
</label>
|
|
1456
1721
|
}
|
|
@@ -1468,7 +1733,7 @@ class SignUpFormComponent {
|
|
|
1468
1733
|
(input)="setValue('email', $any($event.target).value)"
|
|
1469
1734
|
/>
|
|
1470
1735
|
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1471
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1736
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1472
1737
|
}
|
|
1473
1738
|
</label>
|
|
1474
1739
|
|
|
@@ -1484,7 +1749,7 @@ class SignUpFormComponent {
|
|
|
1484
1749
|
(input)="setValue('password', $any($event.target).value)"
|
|
1485
1750
|
/>
|
|
1486
1751
|
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1487
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1752
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1488
1753
|
}
|
|
1489
1754
|
</label>
|
|
1490
1755
|
|
|
@@ -1511,7 +1776,7 @@ class SignUpFormComponent {
|
|
|
1511
1776
|
}.
|
|
1512
1777
|
</span>
|
|
1513
1778
|
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1514
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1779
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1515
1780
|
}
|
|
1516
1781
|
</label>
|
|
1517
1782
|
}
|
|
@@ -1528,6 +1793,7 @@ class SignUpFormComponent {
|
|
|
1528
1793
|
{{ t('signUp.submit') }}
|
|
1529
1794
|
</button>
|
|
1530
1795
|
</form>
|
|
1796
|
+
}
|
|
1531
1797
|
</div>
|
|
1532
1798
|
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1533
1799
|
}
|
|
@@ -1542,8 +1808,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1542
1808
|
<div [class]="cls('card', 'atlas-card')">
|
|
1543
1809
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1544
1810
|
|
|
1811
|
+
@if (emailUnavailable()) {
|
|
1812
|
+
<!-- The app cannot send the verification email, so an email + password
|
|
1813
|
+
sign-up could never complete: say so instead of offering the form. -->
|
|
1814
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert" data-atlas-email-unavailable="">
|
|
1815
|
+
{{ t('signUp.emailUnavailable') }}
|
|
1816
|
+
</p>
|
|
1817
|
+
} @else {
|
|
1545
1818
|
@for (error of formErrors(); track error.code) {
|
|
1546
|
-
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error
|
|
1819
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ errorText(error) }}</p>
|
|
1547
1820
|
}
|
|
1548
1821
|
|
|
1549
1822
|
<form (submit)="onSubmit($event)">
|
|
@@ -1566,7 +1839,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1566
1839
|
}
|
|
1567
1840
|
</span>
|
|
1568
1841
|
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1569
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1842
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1570
1843
|
}
|
|
1571
1844
|
</div>
|
|
1572
1845
|
} @else {
|
|
@@ -1614,7 +1887,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1614
1887
|
/>
|
|
1615
1888
|
}
|
|
1616
1889
|
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1617
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1890
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1618
1891
|
}
|
|
1619
1892
|
</label>
|
|
1620
1893
|
}
|
|
@@ -1632,7 +1905,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1632
1905
|
(input)="setValue('email', $any($event.target).value)"
|
|
1633
1906
|
/>
|
|
1634
1907
|
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1635
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1908
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1636
1909
|
}
|
|
1637
1910
|
</label>
|
|
1638
1911
|
|
|
@@ -1648,7 +1921,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1648
1921
|
(input)="setValue('password', $any($event.target).value)"
|
|
1649
1922
|
/>
|
|
1650
1923
|
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1651
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1924
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1652
1925
|
}
|
|
1653
1926
|
</label>
|
|
1654
1927
|
|
|
@@ -1675,7 +1948,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1675
1948
|
}.
|
|
1676
1949
|
</span>
|
|
1677
1950
|
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1678
|
-
<span [class]="cls('formFieldError', 'atlas-error')">{{ error
|
|
1951
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ errorText(error) }}</span>
|
|
1679
1952
|
}
|
|
1680
1953
|
</label>
|
|
1681
1954
|
}
|
|
@@ -1692,6 +1965,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1692
1965
|
{{ t('signUp.submit') }}
|
|
1693
1966
|
</button>
|
|
1694
1967
|
</form>
|
|
1968
|
+
}
|
|
1695
1969
|
</div>
|
|
1696
1970
|
`,
|
|
1697
1971
|
}]
|
|
@@ -1751,7 +2025,7 @@ class SignUpComponent {
|
|
|
1751
2025
|
(submitSignUp)="flow.create($event.values, $event.consent)"
|
|
1752
2026
|
></atlas-sign-up-form>
|
|
1753
2027
|
}
|
|
1754
|
-
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey", "captcha"], outputs: ["submitFlow"] }, { kind: "component", type: SignUpFormComponent, selector: "atlas-sign-up-form", inputs: ["settings", "busy", "errors", "captcha"], outputs: ["submitSignUp"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2028
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey", "captcha", "firstFactors", "preparedFactor"], outputs: ["submitFlow", "chooseFactor"] }, { kind: "component", type: SignUpFormComponent, selector: "atlas-sign-up-form", inputs: ["settings", "busy", "errors", "captcha"], outputs: ["submitSignUp"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1755
2029
|
}
|
|
1756
2030
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
|
|
1757
2031
|
type: Component,
|