@atlasauth/angular 0.2.1 → 0.4.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 +108 -0
- package/README.md +1 -1
- package/dist/README.md +1 -1
- package/dist/esm2022/lib/atlas.config.mjs +1 -1
- package/dist/esm2022/lib/atlas.service.mjs +92 -33
- package/dist/esm2022/lib/components/attempt-flow.component.mjs +94 -5
- package/dist/esm2022/lib/components/flow-controller.mjs +130 -7
- package/dist/esm2022/lib/components/sign-in.component.mjs +20 -4
- package/dist/esm2022/lib/components/sign-up-form.component.mjs +432 -0
- package/dist/esm2022/lib/components/sign-up.component.mjs +52 -22
- package/dist/esm2022/lib/directives/captcha-slot.directive.mjs +42 -0
- package/dist/esm2022/lib/i18n.mjs +16 -1
- package/dist/esm2022/public-api.mjs +6 -1
- package/dist/fesm2022/atlasauth-angular.mjs +860 -67
- package/dist/fesm2022/atlasauth-angular.mjs.map +1 -1
- package/dist/lib/atlas.config.d.ts +7 -2
- package/dist/lib/atlas.service.d.ts +23 -3
- package/dist/lib/components/attempt-flow.component.d.ts +22 -2
- package/dist/lib/components/flow-controller.d.ts +66 -3
- package/dist/lib/components/sign-in.component.d.ts +3 -1
- package/dist/lib/components/sign-up-form.component.d.ts +47 -0
- package/dist/lib/components/sign-up.component.d.ts +9 -5
- package/dist/lib/directives/captcha-slot.directive.d.ts +19 -0
- package/dist/public-api.d.ts +4 -0
- package/package.json +7 -5
|
@@ -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, FapiClient,
|
|
8
|
-
export { FapiClient, advance, flowFromPending, getProviderToken, initialFlow, pollAttempt, prepareFactor, requestForStep, shouldKeepPolling } from '@atlasauth/js';
|
|
7
|
+
import { TokenCache, resolveFrontendApi, FapiClient, SignOutGuard, 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,12 +167,27 @@ 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',
|
|
173
174
|
'signUp.submit': 'Continue',
|
|
174
175
|
'signUp.haveAccount': 'Already have an account?',
|
|
175
176
|
'signUp.signInLink': 'Sign in',
|
|
177
|
+
'signUp.emailLabel': 'Email address',
|
|
178
|
+
'signUp.passwordLabel': 'Password',
|
|
179
|
+
'signUp.optionalField': '{label} (optional)',
|
|
180
|
+
'signUp.selectPlaceholder': 'Select…',
|
|
181
|
+
'signUp.consentAgree': 'I agree to the',
|
|
182
|
+
'signUp.consentNotice': 'By continuing you agree to the',
|
|
183
|
+
'signUp.terms': 'Terms of Service',
|
|
184
|
+
'signUp.privacy': 'Privacy Policy',
|
|
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.",
|
|
188
|
+
'captcha.prompt': 'Complete the security check to continue.',
|
|
189
|
+
'captcha.required': 'Please complete the security check, then try again.',
|
|
190
|
+
'captcha.failed': "The security check didn't pass. Please try again.",
|
|
176
191
|
'mfa.title': 'Two-step verification',
|
|
177
192
|
'mfa.subtitle': 'Enter the code from your authenticator app.',
|
|
178
193
|
'mfa.codeLabel': 'Code',
|
|
@@ -250,6 +265,14 @@ class AtlasService {
|
|
|
250
265
|
cache = new TokenCache();
|
|
251
266
|
fapiClient;
|
|
252
267
|
refreshTimer = null;
|
|
268
|
+
/**
|
|
269
|
+
* The sessions this browser signed out of (tombstones in localStorage, per
|
|
270
|
+
* Frontend API origin), the revoke and its background re-tries. Every reload /
|
|
271
|
+
* refresh asks it before adopting a session, so a sign-out the server never
|
|
272
|
+
* confirmed is not undone by the still-valid refresh cookie.
|
|
273
|
+
*/
|
|
274
|
+
guard;
|
|
275
|
+
stopWatchingSignOut = null;
|
|
253
276
|
catalog;
|
|
254
277
|
/**
|
|
255
278
|
* A sign-in attempt handed back by an OAuth redirect that still needs a second
|
|
@@ -299,7 +322,11 @@ class AtlasService {
|
|
|
299
322
|
constructor() {
|
|
300
323
|
const options = this.options;
|
|
301
324
|
this.publishableKey = options.publishableKey;
|
|
302
|
-
|
|
325
|
+
// Throws an `AtlasConfigError` on a missing/invalid value rather than quietly
|
|
326
|
+
// sending every request to a relative path on the app's origin.
|
|
327
|
+
this.baseUrl = resolveFrontendApi(options.frontendApi, {
|
|
328
|
+
component: 'provideAtlas() / AtlasModule.forRoot()',
|
|
329
|
+
});
|
|
303
330
|
this.doFetch = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
304
331
|
this.catalog = options.localization ? withFallback(options.localization) : EN_US;
|
|
305
332
|
this.fapiClient = new FapiClient({
|
|
@@ -307,6 +334,10 @@ class AtlasService {
|
|
|
307
334
|
baseUrl: this.baseUrl,
|
|
308
335
|
fetchImpl: this.doFetch,
|
|
309
336
|
});
|
|
337
|
+
this.guard = new SignOutGuard({
|
|
338
|
+
request: (path, init) => this.call(path, init),
|
|
339
|
+
frontendApi: this.baseUrl,
|
|
340
|
+
});
|
|
310
341
|
// Captured synchronously (not in an async init) so it is available before any
|
|
311
342
|
// flow component reads it: a needs_second_factor redirect carries a status but
|
|
312
343
|
// no ticket, and that is what the 2FA resume seeds from.
|
|
@@ -314,10 +345,19 @@ class AtlasService {
|
|
|
314
345
|
// Boot once, browser only. On the server there is no cookie jar to read a
|
|
315
346
|
// session from, so we leave the state at `loading` and let hydration boot it.
|
|
316
347
|
if (this.isBrowser) {
|
|
348
|
+
// Another tab signed out of THIS session (its tombstone reached the shared
|
|
349
|
+
// store): drop the token and re-check, which then refuses the session.
|
|
350
|
+
this.stopWatchingSignOut = this.guard.watch(() => this.cache.peek()?.sessionId ?? this._session.value?.id, () => {
|
|
351
|
+
this.cache.clear();
|
|
352
|
+
void this.reload();
|
|
353
|
+
});
|
|
317
354
|
void this.boot();
|
|
318
355
|
}
|
|
319
356
|
}
|
|
320
357
|
ngOnDestroy() {
|
|
358
|
+
this.stopWatchingSignOut?.();
|
|
359
|
+
this.stopWatchingSignOut = null;
|
|
360
|
+
this.guard.dispose();
|
|
321
361
|
this.clearRefreshTimer();
|
|
322
362
|
this._status.complete();
|
|
323
363
|
this._user.complete();
|
|
@@ -396,25 +436,27 @@ class AtlasService {
|
|
|
396
436
|
has(condition) {
|
|
397
437
|
return evaluate(this._claims.value, condition).allowed;
|
|
398
438
|
}
|
|
399
|
-
/**
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.
|
|
415
|
-
this.
|
|
416
|
-
this.
|
|
417
|
-
this.
|
|
439
|
+
/**
|
|
440
|
+
* Revoke the current session server-side and drop all local auth state.
|
|
441
|
+
*
|
|
442
|
+
* The session id is remembered before the revoke (a tombstone shared by every
|
|
443
|
+
* tab), so a reload, a token refresh, a sibling tab or a tab opened later never
|
|
444
|
+
* re-adopts it — even when the revoke failed (it is retried once, then again in
|
|
445
|
+
* the background) and the refresh cookie is still valid. Resolves
|
|
446
|
+
* `{ revoked, activeSessionId? }`; with `{ strict: true }` an unconfirmed revoke
|
|
447
|
+
* rejects with a `SignOutError` instead (after the local sign-out). A parked
|
|
448
|
+
* session the server promoted on revoke is adopted. Front-channel logout runs
|
|
449
|
+
* unless `{ frontchannel: false }`.
|
|
450
|
+
*/
|
|
451
|
+
async signOut(options = {}) {
|
|
452
|
+
// With no cached token the guard finds the cookie session itself
|
|
453
|
+
// (GET /v1/client), so a missing JWT never skips the revoke.
|
|
454
|
+
const sessionId = this.cache.peek()?.sessionId ?? this._session.value?.id ?? null;
|
|
455
|
+
const attempt = await this.guard.revoke(sessionId, options);
|
|
456
|
+
this.clearLocalState();
|
|
457
|
+
if (this.guard.promoted(attempt))
|
|
458
|
+
await this.reload();
|
|
459
|
+
return this.guard.settle(attempt, options);
|
|
418
460
|
}
|
|
419
461
|
/**
|
|
420
462
|
* Switch the active organization (or `null` for the personal account). The
|
|
@@ -442,14 +484,10 @@ class AtlasService {
|
|
|
442
484
|
if (!response.ok)
|
|
443
485
|
throw new Error('boot failed');
|
|
444
486
|
const body = (await response.json());
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
this.
|
|
449
|
-
this._user.next(null);
|
|
450
|
-
this._session.next(null);
|
|
451
|
-
this._claims.next(null);
|
|
452
|
-
this._memberships.next([]);
|
|
487
|
+
// A session this browser signed out of is never re-adopted from the
|
|
488
|
+
// refresh cookie; the guard re-tries its revoke in the background.
|
|
489
|
+
if (!body.session || !body.user || this.guard.refuse(body.session.id)) {
|
|
490
|
+
this.clearLocalState();
|
|
453
491
|
return;
|
|
454
492
|
}
|
|
455
493
|
this._user.next(body.user);
|
|
@@ -477,17 +515,41 @@ class AtlasService {
|
|
|
477
515
|
* has no ticket, so nothing is exchanged and the app boots signed-out.
|
|
478
516
|
*/
|
|
479
517
|
async boot() {
|
|
480
|
-
const
|
|
518
|
+
const fromUrl = readRedirectResult(window.location.search);
|
|
519
|
+
let result = fromUrl;
|
|
520
|
+
// §oauth popup mode: this page may be the landing of a sign-in popup whose
|
|
521
|
+
// opener a Cross-Origin-Opener-Policy severed (`#atlas_popup_result=…`).
|
|
522
|
+
// The result is relayed to the opener, which redeems the ticket, and this
|
|
523
|
+
// window closes. If no opener takes it (its tab is gone), finish here like
|
|
524
|
+
// a redirect.
|
|
525
|
+
const landing = await completeOAuthPopupLanding().catch(() => null);
|
|
526
|
+
if (landing && !landing.delivered) {
|
|
527
|
+
const outcome = mapOAuthPopupPayload(landing.payload);
|
|
528
|
+
if (outcome?.ticket && outcome.attemptId) {
|
|
529
|
+
result = { attemptId: outcome.attemptId, ticket: outcome.ticket };
|
|
530
|
+
}
|
|
531
|
+
else if (outcome?.continuationUrl) {
|
|
532
|
+
// A second factor / connect step / provider error: the redirect flow's page.
|
|
533
|
+
window.location.replace(outcome.continuationUrl);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
let exchanged = false;
|
|
481
538
|
if (result?.ticket) {
|
|
482
|
-
await this.call('/v1/client/tickets/exchange', {
|
|
539
|
+
const response = await this.call('/v1/client/tickets/exchange', {
|
|
483
540
|
method: 'POST',
|
|
484
541
|
body: JSON.stringify({ attempt_id: result.attemptId, ticket: result.ticket }),
|
|
485
542
|
}).catch(() => undefined);
|
|
543
|
+
exchanged = !!response?.ok;
|
|
486
544
|
}
|
|
487
|
-
if (
|
|
545
|
+
if (fromUrl) {
|
|
488
546
|
window.history.replaceState({}, document.title, stripRedirectParams(window.location.href));
|
|
489
547
|
}
|
|
490
548
|
await this.reload();
|
|
549
|
+
// Signed in inside the Atlas popup (the redirect flow ran in it): hand back
|
|
550
|
+
// to the waiting opener and close. A no-op in an ordinary tab.
|
|
551
|
+
if (exchanged)
|
|
552
|
+
void closeOAuthPopupAfterSignIn();
|
|
491
553
|
}
|
|
492
554
|
readPendingAttempt() {
|
|
493
555
|
const result = readRedirectResult(window.location.search);
|
|
@@ -509,6 +571,9 @@ class AtlasService {
|
|
|
509
571
|
});
|
|
510
572
|
}
|
|
511
573
|
applyToken(jwt, sessionId) {
|
|
574
|
+
// An in-flight refresh must not resurrect a session signed out meanwhile.
|
|
575
|
+
if (this.guard.isSignedOut(sessionId))
|
|
576
|
+
return;
|
|
512
577
|
const expiresAt = readExpiry(jwt);
|
|
513
578
|
if (!expiresAt)
|
|
514
579
|
return;
|
|
@@ -534,6 +599,15 @@ class AtlasService {
|
|
|
534
599
|
await this.reload();
|
|
535
600
|
}
|
|
536
601
|
}
|
|
602
|
+
clearLocalState() {
|
|
603
|
+
this.clearRefreshTimer();
|
|
604
|
+
this.cache.clear();
|
|
605
|
+
this._status.next('signed_out');
|
|
606
|
+
this._user.next(null);
|
|
607
|
+
this._session.next(null);
|
|
608
|
+
this._claims.next(null);
|
|
609
|
+
this._memberships.next([]);
|
|
610
|
+
}
|
|
537
611
|
/** §7.4: proactive, jittered refresh scheduled from the token's own expiry. */
|
|
538
612
|
scheduleRefresh() {
|
|
539
613
|
this.clearRefreshTimer();
|
|
@@ -823,6 +897,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
823
897
|
}]
|
|
824
898
|
}] });
|
|
825
899
|
|
|
900
|
+
/**
|
|
901
|
+
* `[atlasCaptchaSlot]="controller"` — the element a flow's captcha widget draws
|
|
902
|
+
* into (see `createCaptchaController` in `@atlasauth/js`). Attaches the
|
|
903
|
+
* controller to the host element while it is rendered and detaches it (removing
|
|
904
|
+
* the widget) when it goes away.
|
|
905
|
+
*/
|
|
906
|
+
class AtlasCaptchaSlotDirective {
|
|
907
|
+
host = inject(ElementRef);
|
|
908
|
+
current = null;
|
|
909
|
+
initialized = false;
|
|
910
|
+
set controller(next) {
|
|
911
|
+
if (next === this.current)
|
|
912
|
+
return;
|
|
913
|
+
if (this.initialized)
|
|
914
|
+
this.current?.attach(null);
|
|
915
|
+
this.current = next;
|
|
916
|
+
if (this.initialized)
|
|
917
|
+
this.current?.attach(this.host.nativeElement);
|
|
918
|
+
}
|
|
919
|
+
ngOnInit() {
|
|
920
|
+
this.initialized = true;
|
|
921
|
+
this.current?.attach(this.host.nativeElement);
|
|
922
|
+
}
|
|
923
|
+
ngOnDestroy() {
|
|
924
|
+
this.current?.attach(null);
|
|
925
|
+
}
|
|
926
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasCaptchaSlotDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
927
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: AtlasCaptchaSlotDirective, isStandalone: true, selector: "[atlasCaptchaSlot]", inputs: { controller: ["atlasCaptchaSlot", "controller"] }, ngImport: i0 });
|
|
928
|
+
}
|
|
929
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasCaptchaSlotDirective, decorators: [{
|
|
930
|
+
type: Directive,
|
|
931
|
+
args: [{
|
|
932
|
+
selector: '[atlasCaptchaSlot]',
|
|
933
|
+
standalone: true,
|
|
934
|
+
}]
|
|
935
|
+
}], propDecorators: { controller: [{
|
|
936
|
+
type: Input,
|
|
937
|
+
args: ['atlasCaptchaSlot']
|
|
938
|
+
}] } });
|
|
939
|
+
|
|
826
940
|
/**
|
|
827
941
|
* The server-driven flow shell, shared by `<atlas-sign-in>` and
|
|
828
942
|
* `<atlas-sign-up>`. The Angular peer of React's internal `AttemptFlow`.
|
|
@@ -839,8 +953,24 @@ class AttemptFlowComponent {
|
|
|
839
953
|
busy = false;
|
|
840
954
|
errors = [];
|
|
841
955
|
titleKey = 'signIn.title';
|
|
956
|
+
/**
|
|
957
|
+
* The sign-in captcha, drawn above the submit button on the identifier step
|
|
958
|
+
* (an always-on sign-in captcha) and on the challenge step the server demands
|
|
959
|
+
* with `needs_captcha`. Null when the flow draws none.
|
|
960
|
+
*/
|
|
961
|
+
captcha = null;
|
|
962
|
+
/**
|
|
963
|
+
* The first factors to offer on the `needs_first_factor` step — pass
|
|
964
|
+
* `AtlasFlowController.firstFactors()`, which drops email code / link while the
|
|
965
|
+
* app cannot deliver email. Null uses the server's list as-is.
|
|
966
|
+
*/
|
|
967
|
+
firstFactors = null;
|
|
968
|
+
/** The first factor already sent (`AtlasFlowController.preparedFactor()`): its code is collected. */
|
|
969
|
+
preparedFactor = null;
|
|
842
970
|
/** Emits the collected field values when the form is submitted. */
|
|
843
971
|
submitFlow = new EventEmitter();
|
|
972
|
+
/** Emits when the user picks a first factor the server must send first (an emailed code). */
|
|
973
|
+
chooseFactor = new EventEmitter();
|
|
844
974
|
values = {};
|
|
845
975
|
t(key, vars) {
|
|
846
976
|
return translate(this.atlas.localization, key, vars);
|
|
@@ -864,8 +994,26 @@ class AttemptFlowComponent {
|
|
|
864
994
|
step() {
|
|
865
995
|
return this.attempt ? nextStep(this.attempt) : { kind: 'collect_identifier' };
|
|
866
996
|
}
|
|
997
|
+
showCaptcha() {
|
|
998
|
+
const kind = this.step().kind;
|
|
999
|
+
return !!this.captcha && (kind === 'collect_identifier' || kind === 'collect_captcha');
|
|
1000
|
+
}
|
|
1001
|
+
/** The first factors on offer at a `needs_first_factor` step (empty on any other). */
|
|
1002
|
+
offered() {
|
|
1003
|
+
const step = this.step();
|
|
1004
|
+
if (step.kind !== 'collect_first_factor')
|
|
1005
|
+
return [];
|
|
1006
|
+
return this.firstFactors ?? step.strategies;
|
|
1007
|
+
}
|
|
1008
|
+
showEmailCodeOption() {
|
|
1009
|
+
return this.preparedFactor !== 'email_code' && this.offered().includes('email_code');
|
|
1010
|
+
}
|
|
867
1011
|
showSubmit() {
|
|
868
1012
|
const kind = this.step().kind;
|
|
1013
|
+
// Nothing to submit on a first-factor step that only offers an emailed code
|
|
1014
|
+
// until that code is sent.
|
|
1015
|
+
if (kind === 'collect_first_factor')
|
|
1016
|
+
return this.fields().length > 0;
|
|
869
1017
|
return kind !== 'done' && kind !== 'await_oauth';
|
|
870
1018
|
}
|
|
871
1019
|
fields() {
|
|
@@ -873,8 +1021,17 @@ class AttemptFlowComponent {
|
|
|
873
1021
|
switch (step.kind) {
|
|
874
1022
|
case 'collect_identifier':
|
|
875
1023
|
return [{ kind: 'input', name: 'identifier', labelKey: 'signIn.identifierLabel', type: 'email' }];
|
|
876
|
-
case 'collect_first_factor':
|
|
877
|
-
|
|
1024
|
+
case 'collect_first_factor': {
|
|
1025
|
+
if (this.preparedFactor === 'email_code') {
|
|
1026
|
+
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
1027
|
+
}
|
|
1028
|
+
// The password field unless the only offer is an emailed code (chosen
|
|
1029
|
+
// with the button below the form).
|
|
1030
|
+
const offered = this.offered();
|
|
1031
|
+
return offered.includes('email_code') && !offered.includes('password')
|
|
1032
|
+
? []
|
|
1033
|
+
: [{ kind: 'input', name: 'password', labelKey: 'signIn.passwordLabel', type: 'password' }];
|
|
1034
|
+
}
|
|
878
1035
|
case 'collect_second_factor':
|
|
879
1036
|
return [{ kind: 'input', name: 'code', labelKey: 'mfa.codeLabel', type: 'text' }];
|
|
880
1037
|
case 'enroll_second_factor':
|
|
@@ -888,6 +1045,10 @@ class AttemptFlowComponent {
|
|
|
888
1045
|
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
889
1046
|
case 'collect_new_password':
|
|
890
1047
|
return [{ kind: 'input', name: 'password', labelKey: 'reset.newPasswordLabel', type: 'password' }];
|
|
1048
|
+
case 'collect_captcha':
|
|
1049
|
+
// §5 the server withheld the attempt until a captcha clears. Solving the
|
|
1050
|
+
// widget resubmits the identifier on its own; the button retries.
|
|
1051
|
+
return [msg('captcha.prompt')];
|
|
891
1052
|
case 'await_oauth':
|
|
892
1053
|
return [msg('signIn.magicLinkSent')];
|
|
893
1054
|
case 'done':
|
|
@@ -901,7 +1062,7 @@ class AttemptFlowComponent {
|
|
|
901
1062
|
}
|
|
902
1063
|
}
|
|
903
1064
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
904
|
-
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" }, outputs: { submitFlow: "submitFlow" }, ngImport: i0, template: `
|
|
1065
|
+
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: `
|
|
905
1066
|
<div [class]="cls('card', 'atlas-card')">
|
|
906
1067
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
907
1068
|
|
|
@@ -917,6 +1078,7 @@ class AttemptFlowComponent {
|
|
|
917
1078
|
<input
|
|
918
1079
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
919
1080
|
[type]="f.type"
|
|
1081
|
+
[attr.name]="f.name"
|
|
920
1082
|
[value]="values[f.name] ?? ''"
|
|
921
1083
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
922
1084
|
/>
|
|
@@ -929,14 +1091,29 @@ class AttemptFlowComponent {
|
|
|
929
1091
|
}
|
|
930
1092
|
}
|
|
931
1093
|
|
|
1094
|
+
@if (showCaptcha()) {
|
|
1095
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1096
|
+
}
|
|
1097
|
+
|
|
932
1098
|
@if (showSubmit()) {
|
|
933
1099
|
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
934
1100
|
{{ t('signIn.submit') }}
|
|
935
1101
|
</button>
|
|
936
1102
|
}
|
|
1103
|
+
|
|
1104
|
+
@if (showEmailCodeOption()) {
|
|
1105
|
+
<button
|
|
1106
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1107
|
+
[disabled]="busy"
|
|
1108
|
+
type="button"
|
|
1109
|
+
(click)="chooseFactor.emit('email_code')"
|
|
1110
|
+
>
|
|
1111
|
+
{{ t('signIn.useEmailCode') }}
|
|
1112
|
+
</button>
|
|
1113
|
+
}
|
|
937
1114
|
</form>
|
|
938
1115
|
</div>
|
|
939
|
-
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1116
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
940
1117
|
}
|
|
941
1118
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, decorators: [{
|
|
942
1119
|
type: Component,
|
|
@@ -944,6 +1121,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
944
1121
|
selector: 'atlas-attempt-flow',
|
|
945
1122
|
standalone: true,
|
|
946
1123
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1124
|
+
imports: [AtlasCaptchaSlotDirective],
|
|
947
1125
|
template: `
|
|
948
1126
|
<div [class]="cls('card', 'atlas-card')">
|
|
949
1127
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
@@ -960,6 +1138,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
960
1138
|
<input
|
|
961
1139
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
962
1140
|
[type]="f.type"
|
|
1141
|
+
[attr.name]="f.name"
|
|
963
1142
|
[value]="values[f.name] ?? ''"
|
|
964
1143
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
965
1144
|
/>
|
|
@@ -972,11 +1151,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
972
1151
|
}
|
|
973
1152
|
}
|
|
974
1153
|
|
|
1154
|
+
@if (showCaptcha()) {
|
|
1155
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1156
|
+
}
|
|
1157
|
+
|
|
975
1158
|
@if (showSubmit()) {
|
|
976
1159
|
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
977
1160
|
{{ t('signIn.submit') }}
|
|
978
1161
|
</button>
|
|
979
1162
|
}
|
|
1163
|
+
|
|
1164
|
+
@if (showEmailCodeOption()) {
|
|
1165
|
+
<button
|
|
1166
|
+
[class]="cls('formButtonSecondary', 'atlas-button-secondary')"
|
|
1167
|
+
[disabled]="busy"
|
|
1168
|
+
type="button"
|
|
1169
|
+
(click)="chooseFactor.emit('email_code')"
|
|
1170
|
+
>
|
|
1171
|
+
{{ t('signIn.useEmailCode') }}
|
|
1172
|
+
</button>
|
|
1173
|
+
}
|
|
980
1174
|
</form>
|
|
981
1175
|
</div>
|
|
982
1176
|
`,
|
|
@@ -989,8 +1183,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
989
1183
|
type: Input
|
|
990
1184
|
}], titleKey: [{
|
|
991
1185
|
type: Input
|
|
1186
|
+
}], captcha: [{
|
|
1187
|
+
type: Input
|
|
1188
|
+
}], firstFactors: [{
|
|
1189
|
+
type: Input
|
|
1190
|
+
}], preparedFactor: [{
|
|
1191
|
+
type: Input
|
|
992
1192
|
}], submitFlow: [{
|
|
993
1193
|
type: Output
|
|
1194
|
+
}], chooseFactor: [{
|
|
1195
|
+
type: Output
|
|
994
1196
|
}] } });
|
|
995
1197
|
/** A read-only message row: `labelKey` holds the i18n key of the text to show. */
|
|
996
1198
|
function msg(labelKey) {
|
|
@@ -1005,18 +1207,115 @@ function msg(labelKey) {
|
|
|
1005
1207
|
* server via `advance`, and this controller only posts whatever the current step
|
|
1006
1208
|
* needs and re-renders the returned status. State lives in a signal so templates
|
|
1007
1209
|
* update whether the app runs zoned or zoneless.
|
|
1210
|
+
*
|
|
1211
|
+
* `<atlas-sign-up>` starts the attempt with {@link create}
|
|
1212
|
+
* (`POST /v1/client/sign_ups`); the emailed-code step then runs through the same
|
|
1213
|
+
* {@link submit}. Both flows draw the instance's captcha (from
|
|
1214
|
+
* {@link loadSettings}) when it is required.
|
|
1008
1215
|
*/
|
|
1009
1216
|
class AtlasFlowController {
|
|
1010
1217
|
client;
|
|
1011
1218
|
reload;
|
|
1219
|
+
t;
|
|
1012
1220
|
state;
|
|
1221
|
+
/**
|
|
1222
|
+
* The instance's public sign-in / sign-up settings (captcha, sign-up fields,
|
|
1223
|
+
* legal consent) from `GET /v1/appearance`; null until {@link loadSettings}
|
|
1224
|
+
* resolves, or when the server could not be reached.
|
|
1225
|
+
*/
|
|
1226
|
+
settings = signal(null);
|
|
1227
|
+
/** The sign-in captcha (identifier step / `needs_captcha` challenge). */
|
|
1228
|
+
captcha = createCaptchaController();
|
|
1229
|
+
/** The sign-up captcha (the create-account step). */
|
|
1230
|
+
signUpCaptcha = createCaptchaController();
|
|
1231
|
+
/** Whether {@link captcha} currently draws a widget (for templates). */
|
|
1232
|
+
captchaDrawn = signal(false);
|
|
1233
|
+
/** Whether {@link signUpCaptcha} currently draws a widget (for templates). */
|
|
1234
|
+
signUpCaptchaDrawn = signal(false);
|
|
1235
|
+
/**
|
|
1236
|
+
* The first factor the server was asked to send ({@link prepare}) on the
|
|
1237
|
+
* current `needs_first_factor` step — the form then collects its code. Null
|
|
1238
|
+
* otherwise.
|
|
1239
|
+
*/
|
|
1240
|
+
preparedFactor = signal(null);
|
|
1013
1241
|
pollTimer = null;
|
|
1014
|
-
|
|
1242
|
+
identifier = '';
|
|
1243
|
+
constructor(client, reload, pending,
|
|
1244
|
+
/** Resolves the captcha messages' catalog keys. Defaults to en-US. */
|
|
1245
|
+
t = (key) => translate(EN_US, key)) {
|
|
1015
1246
|
this.client = client;
|
|
1016
1247
|
this.reload = reload;
|
|
1248
|
+
this.t = t;
|
|
1017
1249
|
// Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
|
|
1018
1250
|
// factor) so the user resumes at the demanded step instead of starting over.
|
|
1019
1251
|
this.state = signal(pending ? flowFromPending(pending) : initialFlow);
|
|
1252
|
+
this.syncCaptcha();
|
|
1253
|
+
}
|
|
1254
|
+
/** Fetch the instance's public settings and draw whatever captcha they require. */
|
|
1255
|
+
async loadSettings() {
|
|
1256
|
+
const loaded = await fetchInstanceSettings(this.client);
|
|
1257
|
+
if (loaded) {
|
|
1258
|
+
this.settings.set(loaded);
|
|
1259
|
+
this.syncCaptcha();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* The first factors to offer on a `needs_first_factor` step: the server's list
|
|
1264
|
+
* without email code / link while the app cannot deliver email
|
|
1265
|
+
* (`settings.emailDelivery.available === false`). Empty on any other step.
|
|
1266
|
+
*/
|
|
1267
|
+
firstFactors() {
|
|
1268
|
+
const attempt = this.state().attempt;
|
|
1269
|
+
const step = attempt ? nextStep(attempt) : null;
|
|
1270
|
+
return step?.kind === 'collect_first_factor'
|
|
1271
|
+
? offeredFirstFactors(step.strategies, this.settings()?.emailDelivery)
|
|
1272
|
+
: [];
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* True when email + password sign-up cannot complete because the app cannot
|
|
1276
|
+
* send the verification email (`settings.emailDelivery.passwordSignUp === false`):
|
|
1277
|
+
* `<atlas-sign-up>` shows the "can't send email" notice instead of the form.
|
|
1278
|
+
*/
|
|
1279
|
+
signUpEmailUnavailable() {
|
|
1280
|
+
return this.settings()?.emailDelivery.passwordSignUp === false;
|
|
1281
|
+
}
|
|
1282
|
+
/** False when a password reset email cannot be delivered — hide "Forgot password?". */
|
|
1283
|
+
passwordResetAvailable() {
|
|
1284
|
+
return this.settings()?.emailDelivery.passwordReset !== false;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Ask the server to email a sign-in code for the current `needs_first_factor`
|
|
1288
|
+
* step (`POST …/prepare_first_factor`); the form then collects the code, which
|
|
1289
|
+
* {@link submit} posts as the `email_code` strategy.
|
|
1290
|
+
*/
|
|
1291
|
+
async prepare(strategy) {
|
|
1292
|
+
if (!this.firstFactors().includes(strategy))
|
|
1293
|
+
return;
|
|
1294
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1295
|
+
const next = await prepareFactor(this.client, this.state(), strategy);
|
|
1296
|
+
this.setState(next);
|
|
1297
|
+
if (!next.errors.length)
|
|
1298
|
+
this.preparedFactor.set(strategy);
|
|
1299
|
+
}
|
|
1300
|
+
/** Keep both captchas in step with the settings and the flow's current step. */
|
|
1301
|
+
syncCaptcha() {
|
|
1302
|
+
const settings = this.settings();
|
|
1303
|
+
const state = this.state();
|
|
1304
|
+
this.captcha.configure(signInCaptchaOptions(settings?.captchaFlows.signIn, state, () => void this.submit({})));
|
|
1305
|
+
const signUpFlow = settings?.captchaFlows.signUp;
|
|
1306
|
+
this.signUpCaptcha.configure({
|
|
1307
|
+
flow: signUpFlow,
|
|
1308
|
+
active: !state.attempt && flowNeedsCaptcha(signUpFlow),
|
|
1309
|
+
});
|
|
1310
|
+
this.captchaDrawn.set(this.captcha.needed);
|
|
1311
|
+
this.signUpCaptchaDrawn.set(this.signUpCaptcha.needed);
|
|
1312
|
+
}
|
|
1313
|
+
setState(next) {
|
|
1314
|
+
this.state.set(next);
|
|
1315
|
+
// A prepared factor belongs to the first-factor step it was sent for.
|
|
1316
|
+
if (next.attempt?.status !== 'needs_first_factor')
|
|
1317
|
+
this.preparedFactor.set(null);
|
|
1318
|
+
this.syncCaptcha();
|
|
1020
1319
|
}
|
|
1021
1320
|
/**
|
|
1022
1321
|
* Advance the flow with the values the user typed. On completion, exchange the
|
|
@@ -1026,8 +1325,31 @@ class AtlasFlowController {
|
|
|
1026
1325
|
*/
|
|
1027
1326
|
async submit(values) {
|
|
1028
1327
|
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1328
|
+
const out = await advanceWithCaptcha(this.client, this.state(), values, {
|
|
1329
|
+
captcha: this.captcha,
|
|
1330
|
+
identifier: this.identifier,
|
|
1331
|
+
t: this.t,
|
|
1332
|
+
});
|
|
1333
|
+
this.identifier = out.identifier;
|
|
1334
|
+
await this.finish(out.state);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Create an account (`POST /v1/client/sign_ups`) from a create-account form's
|
|
1338
|
+
* values — email, password and any configured sign-up fields — carrying the
|
|
1339
|
+
* sign-up captcha's token when the instance requires one. The emailed code is
|
|
1340
|
+
* then posted with {@link submit}.
|
|
1341
|
+
*/
|
|
1342
|
+
async create(values, consent = true) {
|
|
1343
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1344
|
+
const next = await createSignUp(this.client, values, {
|
|
1345
|
+
consent,
|
|
1346
|
+
captcha: this.signUpCaptcha,
|
|
1347
|
+
t: this.t,
|
|
1348
|
+
});
|
|
1349
|
+
await this.finish(next);
|
|
1350
|
+
}
|
|
1351
|
+
async finish(next) {
|
|
1352
|
+
this.setState(next);
|
|
1031
1353
|
if (next.attempt?.status === 'complete') {
|
|
1032
1354
|
if (next.ticket) {
|
|
1033
1355
|
await this.client.post('/v1/client/tickets/exchange', {
|
|
@@ -1057,7 +1379,7 @@ class AtlasFlowController {
|
|
|
1057
1379
|
this.pollTimer = setInterval(() => {
|
|
1058
1380
|
void (async () => {
|
|
1059
1381
|
const { state: next, ticket } = await pollAttempt(this.client, this.state());
|
|
1060
|
-
this.
|
|
1382
|
+
this.setState(next);
|
|
1061
1383
|
if (!ticket || !next.attempt)
|
|
1062
1384
|
return;
|
|
1063
1385
|
// The ticket is exchanged by a normal request, so no session token ever
|
|
@@ -1077,15 +1399,19 @@ class AtlasFlowController {
|
|
|
1077
1399
|
this.pollTimer = null;
|
|
1078
1400
|
}
|
|
1079
1401
|
}
|
|
1080
|
-
/** Release the poll timer — call from the component's `ngOnDestroy`. */
|
|
1402
|
+
/** Release the poll timer and the captcha widgets — call from the component's `ngOnDestroy`. */
|
|
1081
1403
|
destroy() {
|
|
1082
1404
|
this.stopPolling();
|
|
1405
|
+
this.captcha.destroy();
|
|
1406
|
+
this.signUpCaptcha.destroy();
|
|
1083
1407
|
}
|
|
1084
1408
|
}
|
|
1085
1409
|
|
|
1086
1410
|
/**
|
|
1087
1411
|
* `<atlas-sign-in>` — the full, multi-step sign-in flow driven entirely by the
|
|
1088
|
-
* server-side attempt status. The Angular peer of React's `<SignIn/>`.
|
|
1412
|
+
* server-side attempt status. The Angular peer of React's `<SignIn/>`. Draws the
|
|
1413
|
+
* instance's sign-in captcha when it is required (always-on, or a
|
|
1414
|
+
* `needs_captcha` challenge).
|
|
1089
1415
|
*
|
|
1090
1416
|
* ```html
|
|
1091
1417
|
* <atlas-sign-in *atlasSignedOut afterUrl="/dashboard"></atlas-sign-in>
|
|
@@ -1097,10 +1423,15 @@ class SignInComponent {
|
|
|
1097
1423
|
afterUrl;
|
|
1098
1424
|
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(),
|
|
1099
1425
|
// Resume mid-flow if an OAuth redirect came back owing a second factor.
|
|
1100
|
-
this.atlas.pendingAttempt);
|
|
1426
|
+
this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
1101
1427
|
ngOnInit() {
|
|
1102
1428
|
// If seeded from a pending attempt that is already pollable, start polling.
|
|
1103
1429
|
this.flow.syncPolling();
|
|
1430
|
+
// §5 the instance's captcha settings (switching captcha on in the dashboard
|
|
1431
|
+
// draws it here with no code change) and email delivery (no emailed-code
|
|
1432
|
+
// option while the app cannot send email).
|
|
1433
|
+
if (typeof window !== 'undefined')
|
|
1434
|
+
void this.flow.loadSettings();
|
|
1104
1435
|
}
|
|
1105
1436
|
ngOnDestroy() {
|
|
1106
1437
|
this.flow.destroy();
|
|
@@ -1112,9 +1443,13 @@ class SignInComponent {
|
|
|
1112
1443
|
[busy]="flow.state().busy"
|
|
1113
1444
|
[errors]="flow.state().errors"
|
|
1114
1445
|
titleKey="signIn.title"
|
|
1446
|
+
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1447
|
+
[firstFactors]="flow.firstFactors()"
|
|
1448
|
+
[preparedFactor]="flow.preparedFactor()"
|
|
1115
1449
|
(submitFlow)="flow.submit($event)"
|
|
1450
|
+
(chooseFactor)="flow.prepare($event)"
|
|
1116
1451
|
></atlas-attempt-flow>
|
|
1117
|
-
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey"], outputs: ["submitFlow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1452
|
+
`, 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 });
|
|
1118
1453
|
}
|
|
1119
1454
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
|
|
1120
1455
|
type: Component,
|
|
@@ -1129,7 +1464,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1129
1464
|
[busy]="flow.state().busy"
|
|
1130
1465
|
[errors]="flow.state().errors"
|
|
1131
1466
|
titleKey="signIn.title"
|
|
1467
|
+
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1468
|
+
[firstFactors]="flow.firstFactors()"
|
|
1469
|
+
[preparedFactor]="flow.preparedFactor()"
|
|
1132
1470
|
(submitFlow)="flow.submit($event)"
|
|
1471
|
+
(chooseFactor)="flow.prepare($event)"
|
|
1133
1472
|
></atlas-attempt-flow>
|
|
1134
1473
|
`,
|
|
1135
1474
|
}]
|
|
@@ -1138,9 +1477,438 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1138
1477
|
}] } });
|
|
1139
1478
|
|
|
1140
1479
|
/**
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1480
|
+
* The create-account step of `<atlas-sign-up>`: the instance's configured extra
|
|
1481
|
+
* fields, email + password, the legal-consent row and — when the instance
|
|
1482
|
+
* requires one on sign-up — the captcha widget. Values live on the component so
|
|
1483
|
+
* a refused submit never clears them. When the app cannot send the verification
|
|
1484
|
+
* email (`settings.emailDelivery.passwordSignUp === false`) it shows the
|
|
1485
|
+
* `signUp.emailUnavailable` notice instead of the form.
|
|
1486
|
+
*/
|
|
1487
|
+
class SignUpFormComponent {
|
|
1488
|
+
atlas = inject(AtlasService);
|
|
1489
|
+
settings = null;
|
|
1490
|
+
busy = false;
|
|
1491
|
+
errors = [];
|
|
1492
|
+
captcha = null;
|
|
1493
|
+
/** Emits the typed values and the consent tick when the form is submitted. */
|
|
1494
|
+
submitSignUp = new EventEmitter();
|
|
1495
|
+
values = {};
|
|
1496
|
+
agreed = false;
|
|
1497
|
+
t(key, vars) {
|
|
1498
|
+
return translate(this.atlas.localization, key, vars);
|
|
1499
|
+
}
|
|
1500
|
+
cls(key, base) {
|
|
1501
|
+
return classFor(key, this.atlas.appearance, base);
|
|
1502
|
+
}
|
|
1503
|
+
setValue(name, value) {
|
|
1504
|
+
this.values = { ...this.values, [name]: value };
|
|
1505
|
+
}
|
|
1506
|
+
/** Email + password sign-up cannot complete: the app cannot send the verification email. */
|
|
1507
|
+
emailUnavailable() {
|
|
1508
|
+
return this.settings?.emailDelivery?.passwordSignUp === false;
|
|
1509
|
+
}
|
|
1510
|
+
fields() {
|
|
1511
|
+
return this.settings?.signUpFields ?? [];
|
|
1512
|
+
}
|
|
1513
|
+
label(f) {
|
|
1514
|
+
return f.required ? f.label : this.t('signUp.optionalField', { label: f.label });
|
|
1515
|
+
}
|
|
1516
|
+
inputType(f) {
|
|
1517
|
+
return ['email', 'tel', 'url', 'number', 'date'].includes(f.type) ? f.type : 'text';
|
|
1518
|
+
}
|
|
1519
|
+
/** Same encoding as React, the embed widget and the hosted page: one value,
|
|
1520
|
+
* or a comma-joined list for multiselect. */
|
|
1521
|
+
picked(key) {
|
|
1522
|
+
const value = this.values[key];
|
|
1523
|
+
return value ? value.split(', ') : [];
|
|
1524
|
+
}
|
|
1525
|
+
toggle(f, option, on) {
|
|
1526
|
+
if (f.type === 'radio') {
|
|
1527
|
+
this.setValue(f.key, option);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const current = this.picked(f.key);
|
|
1531
|
+
this.setValue(f.key, (on ? [...current, option] : current.filter((p) => p !== option)).join(', '));
|
|
1532
|
+
}
|
|
1533
|
+
showLegal() {
|
|
1534
|
+
const legal = this.settings?.legal;
|
|
1535
|
+
return !!legal && (!!legal.termsUrl || !!legal.privacyUrl);
|
|
1536
|
+
}
|
|
1537
|
+
consentOk() {
|
|
1538
|
+
return !this.settings?.legal?.required || this.agreed;
|
|
1539
|
+
}
|
|
1540
|
+
formErrors() {
|
|
1541
|
+
return this.errors.filter((error) => !error.param);
|
|
1542
|
+
}
|
|
1543
|
+
fieldErrorsFor(name) {
|
|
1544
|
+
return this.errors.filter((error) => error.param === name);
|
|
1545
|
+
}
|
|
1546
|
+
onSubmit(event) {
|
|
1547
|
+
event.preventDefault();
|
|
1548
|
+
if (this.emailUnavailable() || !this.consentOk())
|
|
1549
|
+
return;
|
|
1550
|
+
this.submitSignUp.emit({
|
|
1551
|
+
values: { ...this.values },
|
|
1552
|
+
consent: this.settings?.legal?.required ? this.agreed : true,
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1556
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SignUpFormComponent, isStandalone: true, selector: "atlas-sign-up-form", inputs: { settings: "settings", busy: "busy", errors: "errors", captcha: "captcha" }, outputs: { submitSignUp: "submitSignUp" }, ngImport: i0, template: `
|
|
1557
|
+
<div [class]="cls('card', 'atlas-card')">
|
|
1558
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1559
|
+
|
|
1560
|
+
@if (emailUnavailable()) {
|
|
1561
|
+
<!-- The app cannot send the verification email, so an email + password
|
|
1562
|
+
sign-up could never complete: say so instead of offering the form. -->
|
|
1563
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert" data-atlas-email-unavailable="">
|
|
1564
|
+
{{ t('signUp.emailUnavailable') }}
|
|
1565
|
+
</p>
|
|
1566
|
+
} @else {
|
|
1567
|
+
@for (error of formErrors(); track error.code) {
|
|
1568
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
<form (submit)="onSubmit($event)">
|
|
1572
|
+
@for (f of fields(); track f.key) {
|
|
1573
|
+
@if (f.type === 'radio' || f.type === 'multiselect') {
|
|
1574
|
+
<div [class]="cls('formField', 'atlas-field')">
|
|
1575
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1576
|
+
<span class="atlas-choices">
|
|
1577
|
+
@for (o of f.options; track o) {
|
|
1578
|
+
<label class="atlas-choice">
|
|
1579
|
+
<input
|
|
1580
|
+
[type]="f.type === 'radio' ? 'radio' : 'checkbox'"
|
|
1581
|
+
[attr.name]="f.key"
|
|
1582
|
+
[value]="o"
|
|
1583
|
+
[checked]="picked(f.key).includes(o)"
|
|
1584
|
+
(change)="toggle(f, o, $any($event.target).checked)"
|
|
1585
|
+
/>
|
|
1586
|
+
{{ o }}
|
|
1587
|
+
</label>
|
|
1588
|
+
}
|
|
1589
|
+
</span>
|
|
1590
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1591
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1592
|
+
}
|
|
1593
|
+
</div>
|
|
1594
|
+
} @else {
|
|
1595
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1596
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1597
|
+
@if (f.type === 'checkbox') {
|
|
1598
|
+
<input
|
|
1599
|
+
type="checkbox"
|
|
1600
|
+
[attr.name]="f.key"
|
|
1601
|
+
[checked]="values[f.key] === 'true'"
|
|
1602
|
+
(change)="setValue(f.key, $any($event.target).checked ? 'true' : '')"
|
|
1603
|
+
/>
|
|
1604
|
+
} @else if (f.type === 'select') {
|
|
1605
|
+
<select
|
|
1606
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1607
|
+
[attr.name]="f.key"
|
|
1608
|
+
[value]="values[f.key] ?? ''"
|
|
1609
|
+
[required]="f.required"
|
|
1610
|
+
(change)="setValue(f.key, $any($event.target).value)"
|
|
1611
|
+
>
|
|
1612
|
+
<option value="">{{ f.placeholder ?? t('signUp.selectPlaceholder') }}</option>
|
|
1613
|
+
@for (o of f.options; track o) {
|
|
1614
|
+
<option [value]="o">{{ o }}</option>
|
|
1615
|
+
}
|
|
1616
|
+
</select>
|
|
1617
|
+
} @else if (f.type === 'textarea') {
|
|
1618
|
+
<textarea
|
|
1619
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1620
|
+
[attr.name]="f.key"
|
|
1621
|
+
rows="3"
|
|
1622
|
+
[value]="values[f.key] ?? ''"
|
|
1623
|
+
[required]="f.required"
|
|
1624
|
+
[attr.placeholder]="f.placeholder"
|
|
1625
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1626
|
+
></textarea>
|
|
1627
|
+
} @else {
|
|
1628
|
+
<input
|
|
1629
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1630
|
+
[type]="inputType(f)"
|
|
1631
|
+
[attr.name]="f.key"
|
|
1632
|
+
[value]="values[f.key] ?? ''"
|
|
1633
|
+
[required]="f.required"
|
|
1634
|
+
[attr.placeholder]="f.placeholder"
|
|
1635
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1636
|
+
/>
|
|
1637
|
+
}
|
|
1638
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1639
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1640
|
+
}
|
|
1641
|
+
</label>
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1646
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.emailLabel') }}</span>
|
|
1647
|
+
<input
|
|
1648
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1649
|
+
type="email"
|
|
1650
|
+
name="email"
|
|
1651
|
+
autocomplete="email"
|
|
1652
|
+
required
|
|
1653
|
+
[value]="values['email'] ?? ''"
|
|
1654
|
+
(input)="setValue('email', $any($event.target).value)"
|
|
1655
|
+
/>
|
|
1656
|
+
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1657
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1658
|
+
}
|
|
1659
|
+
</label>
|
|
1660
|
+
|
|
1661
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1662
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
1663
|
+
<input
|
|
1664
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1665
|
+
type="password"
|
|
1666
|
+
name="password"
|
|
1667
|
+
autocomplete="new-password"
|
|
1668
|
+
required
|
|
1669
|
+
[value]="values['password'] ?? ''"
|
|
1670
|
+
(input)="setValue('password', $any($event.target).value)"
|
|
1671
|
+
/>
|
|
1672
|
+
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1673
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1674
|
+
}
|
|
1675
|
+
</label>
|
|
1676
|
+
|
|
1677
|
+
@if (showLegal()) {
|
|
1678
|
+
<label [class]="cls('formField', 'atlas-consent')">
|
|
1679
|
+
@if (settings?.legal?.required) {
|
|
1680
|
+
<input
|
|
1681
|
+
type="checkbox"
|
|
1682
|
+
name="consent"
|
|
1683
|
+
[checked]="agreed"
|
|
1684
|
+
(change)="agreed = $any($event.target).checked"
|
|
1685
|
+
/>
|
|
1686
|
+
}
|
|
1687
|
+
<span class="atlas-text">
|
|
1688
|
+
{{ t(settings?.legal?.required ? 'signUp.consentAgree' : 'signUp.consentNotice') }}
|
|
1689
|
+
@if (settings?.legal?.termsUrl; as terms) {
|
|
1690
|
+
<a class="atlas-link" [href]="terms" target="_blank" rel="noopener noreferrer">{{ t('signUp.terms') }}</a>
|
|
1691
|
+
}
|
|
1692
|
+
@if (settings?.legal?.termsUrl && settings?.legal?.privacyUrl) {
|
|
1693
|
+
{{ t('signUp.and') }}
|
|
1694
|
+
}
|
|
1695
|
+
@if (settings?.legal?.privacyUrl; as privacy) {
|
|
1696
|
+
<a class="atlas-link" [href]="privacy" target="_blank" rel="noopener noreferrer">{{ t('signUp.privacy') }}</a>
|
|
1697
|
+
}.
|
|
1698
|
+
</span>
|
|
1699
|
+
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1700
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1701
|
+
}
|
|
1702
|
+
</label>
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
@if (captcha) {
|
|
1706
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
<button
|
|
1710
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
1711
|
+
[disabled]="busy || !consentOk()"
|
|
1712
|
+
type="submit"
|
|
1713
|
+
>
|
|
1714
|
+
{{ t('signUp.submit') }}
|
|
1715
|
+
</button>
|
|
1716
|
+
</form>
|
|
1717
|
+
}
|
|
1718
|
+
</div>
|
|
1719
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1720
|
+
}
|
|
1721
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, decorators: [{
|
|
1722
|
+
type: Component,
|
|
1723
|
+
args: [{
|
|
1724
|
+
selector: 'atlas-sign-up-form',
|
|
1725
|
+
standalone: true,
|
|
1726
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1727
|
+
imports: [AtlasCaptchaSlotDirective],
|
|
1728
|
+
template: `
|
|
1729
|
+
<div [class]="cls('card', 'atlas-card')">
|
|
1730
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1731
|
+
|
|
1732
|
+
@if (emailUnavailable()) {
|
|
1733
|
+
<!-- The app cannot send the verification email, so an email + password
|
|
1734
|
+
sign-up could never complete: say so instead of offering the form. -->
|
|
1735
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert" data-atlas-email-unavailable="">
|
|
1736
|
+
{{ t('signUp.emailUnavailable') }}
|
|
1737
|
+
</p>
|
|
1738
|
+
} @else {
|
|
1739
|
+
@for (error of formErrors(); track error.code) {
|
|
1740
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
<form (submit)="onSubmit($event)">
|
|
1744
|
+
@for (f of fields(); track f.key) {
|
|
1745
|
+
@if (f.type === 'radio' || f.type === 'multiselect') {
|
|
1746
|
+
<div [class]="cls('formField', 'atlas-field')">
|
|
1747
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1748
|
+
<span class="atlas-choices">
|
|
1749
|
+
@for (o of f.options; track o) {
|
|
1750
|
+
<label class="atlas-choice">
|
|
1751
|
+
<input
|
|
1752
|
+
[type]="f.type === 'radio' ? 'radio' : 'checkbox'"
|
|
1753
|
+
[attr.name]="f.key"
|
|
1754
|
+
[value]="o"
|
|
1755
|
+
[checked]="picked(f.key).includes(o)"
|
|
1756
|
+
(change)="toggle(f, o, $any($event.target).checked)"
|
|
1757
|
+
/>
|
|
1758
|
+
{{ o }}
|
|
1759
|
+
</label>
|
|
1760
|
+
}
|
|
1761
|
+
</span>
|
|
1762
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1763
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1764
|
+
}
|
|
1765
|
+
</div>
|
|
1766
|
+
} @else {
|
|
1767
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1768
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1769
|
+
@if (f.type === 'checkbox') {
|
|
1770
|
+
<input
|
|
1771
|
+
type="checkbox"
|
|
1772
|
+
[attr.name]="f.key"
|
|
1773
|
+
[checked]="values[f.key] === 'true'"
|
|
1774
|
+
(change)="setValue(f.key, $any($event.target).checked ? 'true' : '')"
|
|
1775
|
+
/>
|
|
1776
|
+
} @else if (f.type === 'select') {
|
|
1777
|
+
<select
|
|
1778
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1779
|
+
[attr.name]="f.key"
|
|
1780
|
+
[value]="values[f.key] ?? ''"
|
|
1781
|
+
[required]="f.required"
|
|
1782
|
+
(change)="setValue(f.key, $any($event.target).value)"
|
|
1783
|
+
>
|
|
1784
|
+
<option value="">{{ f.placeholder ?? t('signUp.selectPlaceholder') }}</option>
|
|
1785
|
+
@for (o of f.options; track o) {
|
|
1786
|
+
<option [value]="o">{{ o }}</option>
|
|
1787
|
+
}
|
|
1788
|
+
</select>
|
|
1789
|
+
} @else if (f.type === 'textarea') {
|
|
1790
|
+
<textarea
|
|
1791
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1792
|
+
[attr.name]="f.key"
|
|
1793
|
+
rows="3"
|
|
1794
|
+
[value]="values[f.key] ?? ''"
|
|
1795
|
+
[required]="f.required"
|
|
1796
|
+
[attr.placeholder]="f.placeholder"
|
|
1797
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1798
|
+
></textarea>
|
|
1799
|
+
} @else {
|
|
1800
|
+
<input
|
|
1801
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1802
|
+
[type]="inputType(f)"
|
|
1803
|
+
[attr.name]="f.key"
|
|
1804
|
+
[value]="values[f.key] ?? ''"
|
|
1805
|
+
[required]="f.required"
|
|
1806
|
+
[attr.placeholder]="f.placeholder"
|
|
1807
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1808
|
+
/>
|
|
1809
|
+
}
|
|
1810
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1811
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1812
|
+
}
|
|
1813
|
+
</label>
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1818
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.emailLabel') }}</span>
|
|
1819
|
+
<input
|
|
1820
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1821
|
+
type="email"
|
|
1822
|
+
name="email"
|
|
1823
|
+
autocomplete="email"
|
|
1824
|
+
required
|
|
1825
|
+
[value]="values['email'] ?? ''"
|
|
1826
|
+
(input)="setValue('email', $any($event.target).value)"
|
|
1827
|
+
/>
|
|
1828
|
+
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1829
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1830
|
+
}
|
|
1831
|
+
</label>
|
|
1832
|
+
|
|
1833
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1834
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
1835
|
+
<input
|
|
1836
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1837
|
+
type="password"
|
|
1838
|
+
name="password"
|
|
1839
|
+
autocomplete="new-password"
|
|
1840
|
+
required
|
|
1841
|
+
[value]="values['password'] ?? ''"
|
|
1842
|
+
(input)="setValue('password', $any($event.target).value)"
|
|
1843
|
+
/>
|
|
1844
|
+
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1845
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1846
|
+
}
|
|
1847
|
+
</label>
|
|
1848
|
+
|
|
1849
|
+
@if (showLegal()) {
|
|
1850
|
+
<label [class]="cls('formField', 'atlas-consent')">
|
|
1851
|
+
@if (settings?.legal?.required) {
|
|
1852
|
+
<input
|
|
1853
|
+
type="checkbox"
|
|
1854
|
+
name="consent"
|
|
1855
|
+
[checked]="agreed"
|
|
1856
|
+
(change)="agreed = $any($event.target).checked"
|
|
1857
|
+
/>
|
|
1858
|
+
}
|
|
1859
|
+
<span class="atlas-text">
|
|
1860
|
+
{{ t(settings?.legal?.required ? 'signUp.consentAgree' : 'signUp.consentNotice') }}
|
|
1861
|
+
@if (settings?.legal?.termsUrl; as terms) {
|
|
1862
|
+
<a class="atlas-link" [href]="terms" target="_blank" rel="noopener noreferrer">{{ t('signUp.terms') }}</a>
|
|
1863
|
+
}
|
|
1864
|
+
@if (settings?.legal?.termsUrl && settings?.legal?.privacyUrl) {
|
|
1865
|
+
{{ t('signUp.and') }}
|
|
1866
|
+
}
|
|
1867
|
+
@if (settings?.legal?.privacyUrl; as privacy) {
|
|
1868
|
+
<a class="atlas-link" [href]="privacy" target="_blank" rel="noopener noreferrer">{{ t('signUp.privacy') }}</a>
|
|
1869
|
+
}.
|
|
1870
|
+
</span>
|
|
1871
|
+
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1872
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1873
|
+
}
|
|
1874
|
+
</label>
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
@if (captcha) {
|
|
1878
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
<button
|
|
1882
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
1883
|
+
[disabled]="busy || !consentOk()"
|
|
1884
|
+
type="submit"
|
|
1885
|
+
>
|
|
1886
|
+
{{ t('signUp.submit') }}
|
|
1887
|
+
</button>
|
|
1888
|
+
</form>
|
|
1889
|
+
}
|
|
1890
|
+
</div>
|
|
1891
|
+
`,
|
|
1892
|
+
}]
|
|
1893
|
+
}], propDecorators: { settings: [{
|
|
1894
|
+
type: Input
|
|
1895
|
+
}], busy: [{
|
|
1896
|
+
type: Input
|
|
1897
|
+
}], errors: [{
|
|
1898
|
+
type: Input
|
|
1899
|
+
}], captcha: [{
|
|
1900
|
+
type: Input
|
|
1901
|
+
}], submitSignUp: [{
|
|
1902
|
+
type: Output
|
|
1903
|
+
}] } });
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* `<atlas-sign-up>` — creates an account. The Angular peer of React's
|
|
1907
|
+
* `<SignUp/>`. The first step is the create-account form (the instance's
|
|
1908
|
+
* configured fields, email, password, legal consent and — when required — the
|
|
1909
|
+
* captcha), which POSTs `/v1/client/sign_ups`; from there it shares the flow
|
|
1910
|
+
* engine with `<atlas-sign-in>` (the emailed-code step, then the ticket
|
|
1911
|
+
* exchange), because the sequence of steps is decided by the server.
|
|
1144
1912
|
*
|
|
1145
1913
|
* ```html
|
|
1146
1914
|
* <atlas-sign-up *atlasSignedOut></atlas-sign-up>
|
|
@@ -1150,20 +1918,35 @@ class SignUpComponent {
|
|
|
1150
1918
|
atlas = inject(AtlasService);
|
|
1151
1919
|
/** Where to send the user once the flow completes. Reserved for host routing. */
|
|
1152
1920
|
afterUrl;
|
|
1153
|
-
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt);
|
|
1921
|
+
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
1922
|
+
ngOnInit() {
|
|
1923
|
+
// §5 the instance's sign-up fields, consent policy and captcha settings.
|
|
1924
|
+
if (typeof window !== 'undefined')
|
|
1925
|
+
void this.flow.loadSettings();
|
|
1926
|
+
}
|
|
1154
1927
|
ngOnDestroy() {
|
|
1155
1928
|
this.flow.destroy();
|
|
1156
1929
|
}
|
|
1157
1930
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1158
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1931
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SignUpComponent, isStandalone: true, selector: "atlas-sign-up", inputs: { afterUrl: "afterUrl" }, ngImport: i0, template: `
|
|
1932
|
+
@if (flow.state().attempt) {
|
|
1933
|
+
<atlas-attempt-flow
|
|
1934
|
+
[attempt]="flow.state().attempt"
|
|
1935
|
+
[busy]="flow.state().busy"
|
|
1936
|
+
[errors]="flow.state().errors"
|
|
1937
|
+
titleKey="signUp.title"
|
|
1938
|
+
(submitFlow)="flow.submit($event)"
|
|
1939
|
+
></atlas-attempt-flow>
|
|
1940
|
+
} @else {
|
|
1941
|
+
<atlas-sign-up-form
|
|
1942
|
+
[settings]="flow.settings()"
|
|
1943
|
+
[busy]="flow.state().busy"
|
|
1944
|
+
[errors]="flow.state().errors"
|
|
1945
|
+
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
1946
|
+
(submitSignUp)="flow.create($event.values, $event.consent)"
|
|
1947
|
+
></atlas-sign-up-form>
|
|
1948
|
+
}
|
|
1949
|
+
`, 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 });
|
|
1167
1950
|
}
|
|
1168
1951
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
|
|
1169
1952
|
type: Component,
|
|
@@ -1171,15 +1954,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1171
1954
|
selector: 'atlas-sign-up',
|
|
1172
1955
|
standalone: true,
|
|
1173
1956
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1174
|
-
imports: [AttemptFlowComponent],
|
|
1957
|
+
imports: [AttemptFlowComponent, SignUpFormComponent],
|
|
1175
1958
|
template: `
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1959
|
+
@if (flow.state().attempt) {
|
|
1960
|
+
<atlas-attempt-flow
|
|
1961
|
+
[attempt]="flow.state().attempt"
|
|
1962
|
+
[busy]="flow.state().busy"
|
|
1963
|
+
[errors]="flow.state().errors"
|
|
1964
|
+
titleKey="signUp.title"
|
|
1965
|
+
(submitFlow)="flow.submit($event)"
|
|
1966
|
+
></atlas-attempt-flow>
|
|
1967
|
+
} @else {
|
|
1968
|
+
<atlas-sign-up-form
|
|
1969
|
+
[settings]="flow.settings()"
|
|
1970
|
+
[busy]="flow.state().busy"
|
|
1971
|
+
[errors]="flow.state().errors"
|
|
1972
|
+
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
1973
|
+
(submitSignUp)="flow.create($event.values, $event.consent)"
|
|
1974
|
+
></atlas-sign-up-form>
|
|
1975
|
+
}
|
|
1183
1976
|
`,
|
|
1184
1977
|
}]
|
|
1185
1978
|
}], propDecorators: { afterUrl: [{
|
|
@@ -1630,5 +2423,5 @@ function atlasChildGuard(options = {}) {
|
|
|
1630
2423
|
* Generated bundle index. Do not edit.
|
|
1631
2424
|
*/
|
|
1632
2425
|
|
|
1633
|
-
export { ATLAS_OPTIONS, ATLAS_UI, AtlasFlowController, AtlasLoadedDirective, AtlasLoadingDirective, AtlasModule, AtlasProtectDirective, AtlasRootDirective, AtlasService, AtlasSignedInDirective, AtlasSignedOutDirective, AttemptFlowComponent, DEFAULT_TOKENS, EN_US, OrganizationProfileComponent, OrganizationSwitcherComponent, SignInComponent, SignUpComponent, THEMES, UserButtonComponent, UserProfileComponent, atlasChildGuard, atlasGuard, classFor, cssVariables, explicitTokens, provideAtlas, renderAppearance, resolveTokens, translate, withFallback };
|
|
2426
|
+
export { ATLAS_OPTIONS, ATLAS_UI, AtlasCaptchaSlotDirective, AtlasFlowController, AtlasLoadedDirective, AtlasLoadingDirective, AtlasModule, AtlasProtectDirective, AtlasRootDirective, AtlasService, AtlasSignedInDirective, AtlasSignedOutDirective, AttemptFlowComponent, DEFAULT_TOKENS, EN_US, OrganizationProfileComponent, OrganizationSwitcherComponent, SignInComponent, SignUpComponent, SignUpFormComponent, THEMES, UserButtonComponent, UserProfileComponent, atlasChildGuard, atlasGuard, classFor, cssVariables, explicitTokens, provideAtlas, renderAppearance, resolveTokens, translate, withFallback };
|
|
1634
2427
|
//# sourceMappingURL=atlasauth-angular.mjs.map
|