@atlasauth/angular 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -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 +7 -3
- package/dist/esm2022/lib/components/attempt-flow.component.mjs +31 -3
- package/dist/esm2022/lib/components/flow-controller.mjs +83 -7
- package/dist/esm2022/lib/components/sign-in.component.mjs +13 -4
- package/dist/esm2022/lib/components/sign-up-form.component.mjs +410 -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 +13 -1
- package/dist/esm2022/public-api.mjs +3 -1
- package/dist/fesm2022/atlasauth-angular.mjs +632 -34
- package/dist/fesm2022/atlasauth-angular.mjs.map +1 -1
- package/dist/lib/atlas.config.d.ts +7 -2
- package/dist/lib/components/attempt-flow.component.d.ts +9 -2
- package/dist/lib/components/flow-controller.d.ts +40 -3
- package/dist/lib/components/sign-in.component.d.ts +3 -1
- package/dist/lib/components/sign-up-form.component.d.ts +43 -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 +3 -0
- package/package.json +5 -4
|
@@ -4,7 +4,7 @@ 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, getProviderToken, runFrontChannelLogoutFromResponse, readRedirectResult, stripRedirectParams, readExpiry, msUntilRefresh, nextStep, flowFromPending, initialFlow,
|
|
7
|
+
import { TokenCache, resolveFrontendApi, FapiClient, getProviderToken, runFrontChannelLogoutFromResponse, readRedirectResult, stripRedirectParams, readExpiry, msUntilRefresh, nextStep, createCaptchaController, flowFromPending, initialFlow, fetchInstanceSettings, signInCaptchaOptions, flowNeedsCaptcha, advanceWithCaptcha, createSignUp, shouldKeepPolling, pollAttempt } from '@atlasauth/js';
|
|
8
8
|
export { FapiClient, 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';
|
|
@@ -173,6 +173,18 @@ const EN_US = {
|
|
|
173
173
|
'signUp.submit': 'Continue',
|
|
174
174
|
'signUp.haveAccount': 'Already have an account?',
|
|
175
175
|
'signUp.signInLink': 'Sign in',
|
|
176
|
+
'signUp.emailLabel': 'Email address',
|
|
177
|
+
'signUp.passwordLabel': 'Password',
|
|
178
|
+
'signUp.optionalField': '{label} (optional)',
|
|
179
|
+
'signUp.selectPlaceholder': 'Select…',
|
|
180
|
+
'signUp.consentAgree': 'I agree to the',
|
|
181
|
+
'signUp.consentNotice': 'By continuing you agree to the',
|
|
182
|
+
'signUp.terms': 'Terms of Service',
|
|
183
|
+
'signUp.privacy': 'Privacy Policy',
|
|
184
|
+
'signUp.and': 'and',
|
|
185
|
+
'captcha.prompt': 'Complete the security check to continue.',
|
|
186
|
+
'captcha.required': 'Please complete the security check, then try again.',
|
|
187
|
+
'captcha.failed': "The security check didn't pass. Please try again.",
|
|
176
188
|
'mfa.title': 'Two-step verification',
|
|
177
189
|
'mfa.subtitle': 'Enter the code from your authenticator app.',
|
|
178
190
|
'mfa.codeLabel': 'Code',
|
|
@@ -299,7 +311,11 @@ class AtlasService {
|
|
|
299
311
|
constructor() {
|
|
300
312
|
const options = this.options;
|
|
301
313
|
this.publishableKey = options.publishableKey;
|
|
302
|
-
|
|
314
|
+
// Throws an `AtlasConfigError` on a missing/invalid value rather than quietly
|
|
315
|
+
// sending every request to a relative path on the app's origin.
|
|
316
|
+
this.baseUrl = resolveFrontendApi(options.frontendApi, {
|
|
317
|
+
component: 'provideAtlas() / AtlasModule.forRoot()',
|
|
318
|
+
});
|
|
303
319
|
this.doFetch = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
304
320
|
this.catalog = options.localization ? withFallback(options.localization) : EN_US;
|
|
305
321
|
this.fapiClient = new FapiClient({
|
|
@@ -823,6 +839,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
823
839
|
}]
|
|
824
840
|
}] });
|
|
825
841
|
|
|
842
|
+
/**
|
|
843
|
+
* `[atlasCaptchaSlot]="controller"` — the element a flow's captcha widget draws
|
|
844
|
+
* into (see `createCaptchaController` in `@atlasauth/js`). Attaches the
|
|
845
|
+
* controller to the host element while it is rendered and detaches it (removing
|
|
846
|
+
* the widget) when it goes away.
|
|
847
|
+
*/
|
|
848
|
+
class AtlasCaptchaSlotDirective {
|
|
849
|
+
host = inject(ElementRef);
|
|
850
|
+
current = null;
|
|
851
|
+
initialized = false;
|
|
852
|
+
set controller(next) {
|
|
853
|
+
if (next === this.current)
|
|
854
|
+
return;
|
|
855
|
+
if (this.initialized)
|
|
856
|
+
this.current?.attach(null);
|
|
857
|
+
this.current = next;
|
|
858
|
+
if (this.initialized)
|
|
859
|
+
this.current?.attach(this.host.nativeElement);
|
|
860
|
+
}
|
|
861
|
+
ngOnInit() {
|
|
862
|
+
this.initialized = true;
|
|
863
|
+
this.current?.attach(this.host.nativeElement);
|
|
864
|
+
}
|
|
865
|
+
ngOnDestroy() {
|
|
866
|
+
this.current?.attach(null);
|
|
867
|
+
}
|
|
868
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasCaptchaSlotDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
869
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: AtlasCaptchaSlotDirective, isStandalone: true, selector: "[atlasCaptchaSlot]", inputs: { controller: ["atlasCaptchaSlot", "controller"] }, ngImport: i0 });
|
|
870
|
+
}
|
|
871
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AtlasCaptchaSlotDirective, decorators: [{
|
|
872
|
+
type: Directive,
|
|
873
|
+
args: [{
|
|
874
|
+
selector: '[atlasCaptchaSlot]',
|
|
875
|
+
standalone: true,
|
|
876
|
+
}]
|
|
877
|
+
}], propDecorators: { controller: [{
|
|
878
|
+
type: Input,
|
|
879
|
+
args: ['atlasCaptchaSlot']
|
|
880
|
+
}] } });
|
|
881
|
+
|
|
826
882
|
/**
|
|
827
883
|
* The server-driven flow shell, shared by `<atlas-sign-in>` and
|
|
828
884
|
* `<atlas-sign-up>`. The Angular peer of React's internal `AttemptFlow`.
|
|
@@ -839,6 +895,12 @@ class AttemptFlowComponent {
|
|
|
839
895
|
busy = false;
|
|
840
896
|
errors = [];
|
|
841
897
|
titleKey = 'signIn.title';
|
|
898
|
+
/**
|
|
899
|
+
* The sign-in captcha, drawn above the submit button on the identifier step
|
|
900
|
+
* (an always-on sign-in captcha) and on the challenge step the server demands
|
|
901
|
+
* with `needs_captcha`. Null when the flow draws none.
|
|
902
|
+
*/
|
|
903
|
+
captcha = null;
|
|
842
904
|
/** Emits the collected field values when the form is submitted. */
|
|
843
905
|
submitFlow = new EventEmitter();
|
|
844
906
|
values = {};
|
|
@@ -864,6 +926,10 @@ class AttemptFlowComponent {
|
|
|
864
926
|
step() {
|
|
865
927
|
return this.attempt ? nextStep(this.attempt) : { kind: 'collect_identifier' };
|
|
866
928
|
}
|
|
929
|
+
showCaptcha() {
|
|
930
|
+
const kind = this.step().kind;
|
|
931
|
+
return !!this.captcha && (kind === 'collect_identifier' || kind === 'collect_captcha');
|
|
932
|
+
}
|
|
867
933
|
showSubmit() {
|
|
868
934
|
const kind = this.step().kind;
|
|
869
935
|
return kind !== 'done' && kind !== 'await_oauth';
|
|
@@ -888,6 +954,10 @@ class AttemptFlowComponent {
|
|
|
888
954
|
return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
|
|
889
955
|
case 'collect_new_password':
|
|
890
956
|
return [{ kind: 'input', name: 'password', labelKey: 'reset.newPasswordLabel', type: 'password' }];
|
|
957
|
+
case 'collect_captcha':
|
|
958
|
+
// §5 the server withheld the attempt until a captcha clears. Solving the
|
|
959
|
+
// widget resubmits the identifier on its own; the button retries.
|
|
960
|
+
return [msg('captcha.prompt')];
|
|
891
961
|
case 'await_oauth':
|
|
892
962
|
return [msg('signIn.magicLinkSent')];
|
|
893
963
|
case 'done':
|
|
@@ -901,7 +971,7 @@ class AttemptFlowComponent {
|
|
|
901
971
|
}
|
|
902
972
|
}
|
|
903
973
|
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: `
|
|
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: `
|
|
905
975
|
<div [class]="cls('card', 'atlas-card')">
|
|
906
976
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
907
977
|
|
|
@@ -917,6 +987,7 @@ class AttemptFlowComponent {
|
|
|
917
987
|
<input
|
|
918
988
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
919
989
|
[type]="f.type"
|
|
990
|
+
[attr.name]="f.name"
|
|
920
991
|
[value]="values[f.name] ?? ''"
|
|
921
992
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
922
993
|
/>
|
|
@@ -929,6 +1000,10 @@ class AttemptFlowComponent {
|
|
|
929
1000
|
}
|
|
930
1001
|
}
|
|
931
1002
|
|
|
1003
|
+
@if (showCaptcha()) {
|
|
1004
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1005
|
+
}
|
|
1006
|
+
|
|
932
1007
|
@if (showSubmit()) {
|
|
933
1008
|
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
934
1009
|
{{ t('signIn.submit') }}
|
|
@@ -936,7 +1011,7 @@ class AttemptFlowComponent {
|
|
|
936
1011
|
}
|
|
937
1012
|
</form>
|
|
938
1013
|
</div>
|
|
939
|
-
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1014
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
940
1015
|
}
|
|
941
1016
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, decorators: [{
|
|
942
1017
|
type: Component,
|
|
@@ -944,6 +1019,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
944
1019
|
selector: 'atlas-attempt-flow',
|
|
945
1020
|
standalone: true,
|
|
946
1021
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1022
|
+
imports: [AtlasCaptchaSlotDirective],
|
|
947
1023
|
template: `
|
|
948
1024
|
<div [class]="cls('card', 'atlas-card')">
|
|
949
1025
|
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
|
|
@@ -960,6 +1036,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
960
1036
|
<input
|
|
961
1037
|
[class]="cls('formFieldInput', 'atlas-input')"
|
|
962
1038
|
[type]="f.type"
|
|
1039
|
+
[attr.name]="f.name"
|
|
963
1040
|
[value]="values[f.name] ?? ''"
|
|
964
1041
|
(input)="setValue(f.name, $any($event.target).value)"
|
|
965
1042
|
/>
|
|
@@ -972,6 +1049,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
972
1049
|
}
|
|
973
1050
|
}
|
|
974
1051
|
|
|
1052
|
+
@if (showCaptcha()) {
|
|
1053
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1054
|
+
}
|
|
1055
|
+
|
|
975
1056
|
@if (showSubmit()) {
|
|
976
1057
|
<button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
|
|
977
1058
|
{{ t('signIn.submit') }}
|
|
@@ -989,6 +1070,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
989
1070
|
type: Input
|
|
990
1071
|
}], titleKey: [{
|
|
991
1072
|
type: Input
|
|
1073
|
+
}], captcha: [{
|
|
1074
|
+
type: Input
|
|
992
1075
|
}], submitFlow: [{
|
|
993
1076
|
type: Output
|
|
994
1077
|
}] } });
|
|
@@ -1005,18 +1088,68 @@ function msg(labelKey) {
|
|
|
1005
1088
|
* server via `advance`, and this controller only posts whatever the current step
|
|
1006
1089
|
* needs and re-renders the returned status. State lives in a signal so templates
|
|
1007
1090
|
* update whether the app runs zoned or zoneless.
|
|
1091
|
+
*
|
|
1092
|
+
* `<atlas-sign-up>` starts the attempt with {@link create}
|
|
1093
|
+
* (`POST /v1/client/sign_ups`); the emailed-code step then runs through the same
|
|
1094
|
+
* {@link submit}. Both flows draw the instance's captcha (from
|
|
1095
|
+
* {@link loadSettings}) when it is required.
|
|
1008
1096
|
*/
|
|
1009
1097
|
class AtlasFlowController {
|
|
1010
1098
|
client;
|
|
1011
1099
|
reload;
|
|
1100
|
+
t;
|
|
1012
1101
|
state;
|
|
1102
|
+
/**
|
|
1103
|
+
* The instance's public sign-in / sign-up settings (captcha, sign-up fields,
|
|
1104
|
+
* legal consent) from `GET /v1/appearance`; null until {@link loadSettings}
|
|
1105
|
+
* resolves, or when the server could not be reached.
|
|
1106
|
+
*/
|
|
1107
|
+
settings = signal(null);
|
|
1108
|
+
/** The sign-in captcha (identifier step / `needs_captcha` challenge). */
|
|
1109
|
+
captcha = createCaptchaController();
|
|
1110
|
+
/** The sign-up captcha (the create-account step). */
|
|
1111
|
+
signUpCaptcha = createCaptchaController();
|
|
1112
|
+
/** Whether {@link captcha} currently draws a widget (for templates). */
|
|
1113
|
+
captchaDrawn = signal(false);
|
|
1114
|
+
/** Whether {@link signUpCaptcha} currently draws a widget (for templates). */
|
|
1115
|
+
signUpCaptchaDrawn = signal(false);
|
|
1013
1116
|
pollTimer = null;
|
|
1014
|
-
|
|
1117
|
+
identifier = '';
|
|
1118
|
+
constructor(client, reload, pending,
|
|
1119
|
+
/** Resolves the captcha messages' catalog keys. Defaults to en-US. */
|
|
1120
|
+
t = (key) => translate(EN_US, key)) {
|
|
1015
1121
|
this.client = client;
|
|
1016
1122
|
this.reload = reload;
|
|
1123
|
+
this.t = t;
|
|
1017
1124
|
// Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
|
|
1018
1125
|
// factor) so the user resumes at the demanded step instead of starting over.
|
|
1019
1126
|
this.state = signal(pending ? flowFromPending(pending) : initialFlow);
|
|
1127
|
+
this.syncCaptcha();
|
|
1128
|
+
}
|
|
1129
|
+
/** Fetch the instance's public settings and draw whatever captcha they require. */
|
|
1130
|
+
async loadSettings() {
|
|
1131
|
+
const loaded = await fetchInstanceSettings(this.client);
|
|
1132
|
+
if (loaded) {
|
|
1133
|
+
this.settings.set(loaded);
|
|
1134
|
+
this.syncCaptcha();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/** Keep both captchas in step with the settings and the flow's current step. */
|
|
1138
|
+
syncCaptcha() {
|
|
1139
|
+
const settings = this.settings();
|
|
1140
|
+
const state = this.state();
|
|
1141
|
+
this.captcha.configure(signInCaptchaOptions(settings?.captchaFlows.signIn, state, () => void this.submit({})));
|
|
1142
|
+
const signUpFlow = settings?.captchaFlows.signUp;
|
|
1143
|
+
this.signUpCaptcha.configure({
|
|
1144
|
+
flow: signUpFlow,
|
|
1145
|
+
active: !state.attempt && flowNeedsCaptcha(signUpFlow),
|
|
1146
|
+
});
|
|
1147
|
+
this.captchaDrawn.set(this.captcha.needed);
|
|
1148
|
+
this.signUpCaptchaDrawn.set(this.signUpCaptcha.needed);
|
|
1149
|
+
}
|
|
1150
|
+
setState(next) {
|
|
1151
|
+
this.state.set(next);
|
|
1152
|
+
this.syncCaptcha();
|
|
1020
1153
|
}
|
|
1021
1154
|
/**
|
|
1022
1155
|
* Advance the flow with the values the user typed. On completion, exchange the
|
|
@@ -1026,8 +1159,31 @@ class AtlasFlowController {
|
|
|
1026
1159
|
*/
|
|
1027
1160
|
async submit(values) {
|
|
1028
1161
|
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1162
|
+
const out = await advanceWithCaptcha(this.client, this.state(), values, {
|
|
1163
|
+
captcha: this.captcha,
|
|
1164
|
+
identifier: this.identifier,
|
|
1165
|
+
t: this.t,
|
|
1166
|
+
});
|
|
1167
|
+
this.identifier = out.identifier;
|
|
1168
|
+
await this.finish(out.state);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Create an account (`POST /v1/client/sign_ups`) from a create-account form's
|
|
1172
|
+
* values — email, password and any configured sign-up fields — carrying the
|
|
1173
|
+
* sign-up captcha's token when the instance requires one. The emailed code is
|
|
1174
|
+
* then posted with {@link submit}.
|
|
1175
|
+
*/
|
|
1176
|
+
async create(values, consent = true) {
|
|
1177
|
+
this.state.update((current) => ({ ...current, busy: true, errors: [] }));
|
|
1178
|
+
const next = await createSignUp(this.client, values, {
|
|
1179
|
+
consent,
|
|
1180
|
+
captcha: this.signUpCaptcha,
|
|
1181
|
+
t: this.t,
|
|
1182
|
+
});
|
|
1183
|
+
await this.finish(next);
|
|
1184
|
+
}
|
|
1185
|
+
async finish(next) {
|
|
1186
|
+
this.setState(next);
|
|
1031
1187
|
if (next.attempt?.status === 'complete') {
|
|
1032
1188
|
if (next.ticket) {
|
|
1033
1189
|
await this.client.post('/v1/client/tickets/exchange', {
|
|
@@ -1057,7 +1213,7 @@ class AtlasFlowController {
|
|
|
1057
1213
|
this.pollTimer = setInterval(() => {
|
|
1058
1214
|
void (async () => {
|
|
1059
1215
|
const { state: next, ticket } = await pollAttempt(this.client, this.state());
|
|
1060
|
-
this.
|
|
1216
|
+
this.setState(next);
|
|
1061
1217
|
if (!ticket || !next.attempt)
|
|
1062
1218
|
return;
|
|
1063
1219
|
// The ticket is exchanged by a normal request, so no session token ever
|
|
@@ -1077,15 +1233,19 @@ class AtlasFlowController {
|
|
|
1077
1233
|
this.pollTimer = null;
|
|
1078
1234
|
}
|
|
1079
1235
|
}
|
|
1080
|
-
/** Release the poll timer — call from the component's `ngOnDestroy`. */
|
|
1236
|
+
/** Release the poll timer and the captcha widgets — call from the component's `ngOnDestroy`. */
|
|
1081
1237
|
destroy() {
|
|
1082
1238
|
this.stopPolling();
|
|
1239
|
+
this.captcha.destroy();
|
|
1240
|
+
this.signUpCaptcha.destroy();
|
|
1083
1241
|
}
|
|
1084
1242
|
}
|
|
1085
1243
|
|
|
1086
1244
|
/**
|
|
1087
1245
|
* `<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/>`.
|
|
1246
|
+
* server-side attempt status. The Angular peer of React's `<SignIn/>`. Draws the
|
|
1247
|
+
* instance's sign-in captcha when it is required (always-on, or a
|
|
1248
|
+
* `needs_captcha` challenge).
|
|
1089
1249
|
*
|
|
1090
1250
|
* ```html
|
|
1091
1251
|
* <atlas-sign-in *atlasSignedOut afterUrl="/dashboard"></atlas-sign-in>
|
|
@@ -1097,10 +1257,14 @@ class SignInComponent {
|
|
|
1097
1257
|
afterUrl;
|
|
1098
1258
|
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(),
|
|
1099
1259
|
// Resume mid-flow if an OAuth redirect came back owing a second factor.
|
|
1100
|
-
this.atlas.pendingAttempt);
|
|
1260
|
+
this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
1101
1261
|
ngOnInit() {
|
|
1102
1262
|
// If seeded from a pending attempt that is already pollable, start polling.
|
|
1103
1263
|
this.flow.syncPolling();
|
|
1264
|
+
// §5 the instance's captcha settings: switching captcha on in the dashboard
|
|
1265
|
+
// draws it here with no code change.
|
|
1266
|
+
if (typeof window !== 'undefined')
|
|
1267
|
+
void this.flow.loadSettings();
|
|
1104
1268
|
}
|
|
1105
1269
|
ngOnDestroy() {
|
|
1106
1270
|
this.flow.destroy();
|
|
@@ -1112,9 +1276,10 @@ class SignInComponent {
|
|
|
1112
1276
|
[busy]="flow.state().busy"
|
|
1113
1277
|
[errors]="flow.state().errors"
|
|
1114
1278
|
titleKey="signIn.title"
|
|
1279
|
+
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1115
1280
|
(submitFlow)="flow.submit($event)"
|
|
1116
1281
|
></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 });
|
|
1282
|
+
`, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey", "captcha"], outputs: ["submitFlow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1118
1283
|
}
|
|
1119
1284
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
|
|
1120
1285
|
type: Component,
|
|
@@ -1129,6 +1294,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1129
1294
|
[busy]="flow.state().busy"
|
|
1130
1295
|
[errors]="flow.state().errors"
|
|
1131
1296
|
titleKey="signIn.title"
|
|
1297
|
+
[captcha]="flow.captchaDrawn() ? flow.captcha : null"
|
|
1132
1298
|
(submitFlow)="flow.submit($event)"
|
|
1133
1299
|
></atlas-attempt-flow>
|
|
1134
1300
|
`,
|
|
@@ -1138,9 +1304,416 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1138
1304
|
}] } });
|
|
1139
1305
|
|
|
1140
1306
|
/**
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1307
|
+
* The create-account step of `<atlas-sign-up>`: the instance's configured extra
|
|
1308
|
+
* fields, email + password, the legal-consent row and — when the instance
|
|
1309
|
+
* requires one on sign-up — the captcha widget. Values live on the component so
|
|
1310
|
+
* a refused submit never clears them.
|
|
1311
|
+
*/
|
|
1312
|
+
class SignUpFormComponent {
|
|
1313
|
+
atlas = inject(AtlasService);
|
|
1314
|
+
settings = null;
|
|
1315
|
+
busy = false;
|
|
1316
|
+
errors = [];
|
|
1317
|
+
captcha = null;
|
|
1318
|
+
/** Emits the typed values and the consent tick when the form is submitted. */
|
|
1319
|
+
submitSignUp = new EventEmitter();
|
|
1320
|
+
values = {};
|
|
1321
|
+
agreed = false;
|
|
1322
|
+
t(key, vars) {
|
|
1323
|
+
return translate(this.atlas.localization, key, vars);
|
|
1324
|
+
}
|
|
1325
|
+
cls(key, base) {
|
|
1326
|
+
return classFor(key, this.atlas.appearance, base);
|
|
1327
|
+
}
|
|
1328
|
+
setValue(name, value) {
|
|
1329
|
+
this.values = { ...this.values, [name]: value };
|
|
1330
|
+
}
|
|
1331
|
+
fields() {
|
|
1332
|
+
return this.settings?.signUpFields ?? [];
|
|
1333
|
+
}
|
|
1334
|
+
label(f) {
|
|
1335
|
+
return f.required ? f.label : this.t('signUp.optionalField', { label: f.label });
|
|
1336
|
+
}
|
|
1337
|
+
inputType(f) {
|
|
1338
|
+
return ['email', 'tel', 'url', 'number', 'date'].includes(f.type) ? f.type : 'text';
|
|
1339
|
+
}
|
|
1340
|
+
/** Same encoding as React, the embed widget and the hosted page: one value,
|
|
1341
|
+
* or a comma-joined list for multiselect. */
|
|
1342
|
+
picked(key) {
|
|
1343
|
+
const value = this.values[key];
|
|
1344
|
+
return value ? value.split(', ') : [];
|
|
1345
|
+
}
|
|
1346
|
+
toggle(f, option, on) {
|
|
1347
|
+
if (f.type === 'radio') {
|
|
1348
|
+
this.setValue(f.key, option);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const current = this.picked(f.key);
|
|
1352
|
+
this.setValue(f.key, (on ? [...current, option] : current.filter((p) => p !== option)).join(', '));
|
|
1353
|
+
}
|
|
1354
|
+
showLegal() {
|
|
1355
|
+
const legal = this.settings?.legal;
|
|
1356
|
+
return !!legal && (!!legal.termsUrl || !!legal.privacyUrl);
|
|
1357
|
+
}
|
|
1358
|
+
consentOk() {
|
|
1359
|
+
return !this.settings?.legal?.required || this.agreed;
|
|
1360
|
+
}
|
|
1361
|
+
formErrors() {
|
|
1362
|
+
return this.errors.filter((error) => !error.param);
|
|
1363
|
+
}
|
|
1364
|
+
fieldErrorsFor(name) {
|
|
1365
|
+
return this.errors.filter((error) => error.param === name);
|
|
1366
|
+
}
|
|
1367
|
+
onSubmit(event) {
|
|
1368
|
+
event.preventDefault();
|
|
1369
|
+
if (!this.consentOk())
|
|
1370
|
+
return;
|
|
1371
|
+
this.submitSignUp.emit({
|
|
1372
|
+
values: { ...this.values },
|
|
1373
|
+
consent: this.settings?.legal?.required ? this.agreed : true,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1377
|
+
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: `
|
|
1378
|
+
<div [class]="cls('card', 'atlas-card')">
|
|
1379
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1380
|
+
|
|
1381
|
+
@for (error of formErrors(); track error.code) {
|
|
1382
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
<form (submit)="onSubmit($event)">
|
|
1386
|
+
@for (f of fields(); track f.key) {
|
|
1387
|
+
@if (f.type === 'radio' || f.type === 'multiselect') {
|
|
1388
|
+
<div [class]="cls('formField', 'atlas-field')">
|
|
1389
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1390
|
+
<span class="atlas-choices">
|
|
1391
|
+
@for (o of f.options; track o) {
|
|
1392
|
+
<label class="atlas-choice">
|
|
1393
|
+
<input
|
|
1394
|
+
[type]="f.type === 'radio' ? 'radio' : 'checkbox'"
|
|
1395
|
+
[attr.name]="f.key"
|
|
1396
|
+
[value]="o"
|
|
1397
|
+
[checked]="picked(f.key).includes(o)"
|
|
1398
|
+
(change)="toggle(f, o, $any($event.target).checked)"
|
|
1399
|
+
/>
|
|
1400
|
+
{{ o }}
|
|
1401
|
+
</label>
|
|
1402
|
+
}
|
|
1403
|
+
</span>
|
|
1404
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1405
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1406
|
+
}
|
|
1407
|
+
</div>
|
|
1408
|
+
} @else {
|
|
1409
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1410
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1411
|
+
@if (f.type === 'checkbox') {
|
|
1412
|
+
<input
|
|
1413
|
+
type="checkbox"
|
|
1414
|
+
[attr.name]="f.key"
|
|
1415
|
+
[checked]="values[f.key] === 'true'"
|
|
1416
|
+
(change)="setValue(f.key, $any($event.target).checked ? 'true' : '')"
|
|
1417
|
+
/>
|
|
1418
|
+
} @else if (f.type === 'select') {
|
|
1419
|
+
<select
|
|
1420
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1421
|
+
[attr.name]="f.key"
|
|
1422
|
+
[value]="values[f.key] ?? ''"
|
|
1423
|
+
[required]="f.required"
|
|
1424
|
+
(change)="setValue(f.key, $any($event.target).value)"
|
|
1425
|
+
>
|
|
1426
|
+
<option value="">{{ f.placeholder ?? t('signUp.selectPlaceholder') }}</option>
|
|
1427
|
+
@for (o of f.options; track o) {
|
|
1428
|
+
<option [value]="o">{{ o }}</option>
|
|
1429
|
+
}
|
|
1430
|
+
</select>
|
|
1431
|
+
} @else if (f.type === 'textarea') {
|
|
1432
|
+
<textarea
|
|
1433
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1434
|
+
[attr.name]="f.key"
|
|
1435
|
+
rows="3"
|
|
1436
|
+
[value]="values[f.key] ?? ''"
|
|
1437
|
+
[required]="f.required"
|
|
1438
|
+
[attr.placeholder]="f.placeholder"
|
|
1439
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1440
|
+
></textarea>
|
|
1441
|
+
} @else {
|
|
1442
|
+
<input
|
|
1443
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1444
|
+
[type]="inputType(f)"
|
|
1445
|
+
[attr.name]="f.key"
|
|
1446
|
+
[value]="values[f.key] ?? ''"
|
|
1447
|
+
[required]="f.required"
|
|
1448
|
+
[attr.placeholder]="f.placeholder"
|
|
1449
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1450
|
+
/>
|
|
1451
|
+
}
|
|
1452
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1453
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1454
|
+
}
|
|
1455
|
+
</label>
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1460
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.emailLabel') }}</span>
|
|
1461
|
+
<input
|
|
1462
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1463
|
+
type="email"
|
|
1464
|
+
name="email"
|
|
1465
|
+
autocomplete="email"
|
|
1466
|
+
required
|
|
1467
|
+
[value]="values['email'] ?? ''"
|
|
1468
|
+
(input)="setValue('email', $any($event.target).value)"
|
|
1469
|
+
/>
|
|
1470
|
+
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1471
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1472
|
+
}
|
|
1473
|
+
</label>
|
|
1474
|
+
|
|
1475
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1476
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
1477
|
+
<input
|
|
1478
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1479
|
+
type="password"
|
|
1480
|
+
name="password"
|
|
1481
|
+
autocomplete="new-password"
|
|
1482
|
+
required
|
|
1483
|
+
[value]="values['password'] ?? ''"
|
|
1484
|
+
(input)="setValue('password', $any($event.target).value)"
|
|
1485
|
+
/>
|
|
1486
|
+
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1487
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1488
|
+
}
|
|
1489
|
+
</label>
|
|
1490
|
+
|
|
1491
|
+
@if (showLegal()) {
|
|
1492
|
+
<label [class]="cls('formField', 'atlas-consent')">
|
|
1493
|
+
@if (settings?.legal?.required) {
|
|
1494
|
+
<input
|
|
1495
|
+
type="checkbox"
|
|
1496
|
+
name="consent"
|
|
1497
|
+
[checked]="agreed"
|
|
1498
|
+
(change)="agreed = $any($event.target).checked"
|
|
1499
|
+
/>
|
|
1500
|
+
}
|
|
1501
|
+
<span class="atlas-text">
|
|
1502
|
+
{{ t(settings?.legal?.required ? 'signUp.consentAgree' : 'signUp.consentNotice') }}
|
|
1503
|
+
@if (settings?.legal?.termsUrl; as terms) {
|
|
1504
|
+
<a class="atlas-link" [href]="terms" target="_blank" rel="noopener noreferrer">{{ t('signUp.terms') }}</a>
|
|
1505
|
+
}
|
|
1506
|
+
@if (settings?.legal?.termsUrl && settings?.legal?.privacyUrl) {
|
|
1507
|
+
{{ t('signUp.and') }}
|
|
1508
|
+
}
|
|
1509
|
+
@if (settings?.legal?.privacyUrl; as privacy) {
|
|
1510
|
+
<a class="atlas-link" [href]="privacy" target="_blank" rel="noopener noreferrer">{{ t('signUp.privacy') }}</a>
|
|
1511
|
+
}.
|
|
1512
|
+
</span>
|
|
1513
|
+
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1514
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1515
|
+
}
|
|
1516
|
+
</label>
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
@if (captcha) {
|
|
1520
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
<button
|
|
1524
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
1525
|
+
[disabled]="busy || !consentOk()"
|
|
1526
|
+
type="submit"
|
|
1527
|
+
>
|
|
1528
|
+
{{ t('signUp.submit') }}
|
|
1529
|
+
</button>
|
|
1530
|
+
</form>
|
|
1531
|
+
</div>
|
|
1532
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1533
|
+
}
|
|
1534
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpFormComponent, decorators: [{
|
|
1535
|
+
type: Component,
|
|
1536
|
+
args: [{
|
|
1537
|
+
selector: 'atlas-sign-up-form',
|
|
1538
|
+
standalone: true,
|
|
1539
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1540
|
+
imports: [AtlasCaptchaSlotDirective],
|
|
1541
|
+
template: `
|
|
1542
|
+
<div [class]="cls('card', 'atlas-card')">
|
|
1543
|
+
<h1 [class]="cls('headerTitle', 'atlas-title')">{{ t('signUp.title') }}</h1>
|
|
1544
|
+
|
|
1545
|
+
@for (error of formErrors(); track error.code) {
|
|
1546
|
+
<p [class]="cls('formFieldError', 'atlas-error')" role="alert">{{ error.message }}</p>
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
<form (submit)="onSubmit($event)">
|
|
1550
|
+
@for (f of fields(); track f.key) {
|
|
1551
|
+
@if (f.type === 'radio' || f.type === 'multiselect') {
|
|
1552
|
+
<div [class]="cls('formField', 'atlas-field')">
|
|
1553
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1554
|
+
<span class="atlas-choices">
|
|
1555
|
+
@for (o of f.options; track o) {
|
|
1556
|
+
<label class="atlas-choice">
|
|
1557
|
+
<input
|
|
1558
|
+
[type]="f.type === 'radio' ? 'radio' : 'checkbox'"
|
|
1559
|
+
[attr.name]="f.key"
|
|
1560
|
+
[value]="o"
|
|
1561
|
+
[checked]="picked(f.key).includes(o)"
|
|
1562
|
+
(change)="toggle(f, o, $any($event.target).checked)"
|
|
1563
|
+
/>
|
|
1564
|
+
{{ o }}
|
|
1565
|
+
</label>
|
|
1566
|
+
}
|
|
1567
|
+
</span>
|
|
1568
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1569
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1570
|
+
}
|
|
1571
|
+
</div>
|
|
1572
|
+
} @else {
|
|
1573
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1574
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ label(f) }}</span>
|
|
1575
|
+
@if (f.type === 'checkbox') {
|
|
1576
|
+
<input
|
|
1577
|
+
type="checkbox"
|
|
1578
|
+
[attr.name]="f.key"
|
|
1579
|
+
[checked]="values[f.key] === 'true'"
|
|
1580
|
+
(change)="setValue(f.key, $any($event.target).checked ? 'true' : '')"
|
|
1581
|
+
/>
|
|
1582
|
+
} @else if (f.type === 'select') {
|
|
1583
|
+
<select
|
|
1584
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1585
|
+
[attr.name]="f.key"
|
|
1586
|
+
[value]="values[f.key] ?? ''"
|
|
1587
|
+
[required]="f.required"
|
|
1588
|
+
(change)="setValue(f.key, $any($event.target).value)"
|
|
1589
|
+
>
|
|
1590
|
+
<option value="">{{ f.placeholder ?? t('signUp.selectPlaceholder') }}</option>
|
|
1591
|
+
@for (o of f.options; track o) {
|
|
1592
|
+
<option [value]="o">{{ o }}</option>
|
|
1593
|
+
}
|
|
1594
|
+
</select>
|
|
1595
|
+
} @else if (f.type === 'textarea') {
|
|
1596
|
+
<textarea
|
|
1597
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1598
|
+
[attr.name]="f.key"
|
|
1599
|
+
rows="3"
|
|
1600
|
+
[value]="values[f.key] ?? ''"
|
|
1601
|
+
[required]="f.required"
|
|
1602
|
+
[attr.placeholder]="f.placeholder"
|
|
1603
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1604
|
+
></textarea>
|
|
1605
|
+
} @else {
|
|
1606
|
+
<input
|
|
1607
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1608
|
+
[type]="inputType(f)"
|
|
1609
|
+
[attr.name]="f.key"
|
|
1610
|
+
[value]="values[f.key] ?? ''"
|
|
1611
|
+
[required]="f.required"
|
|
1612
|
+
[attr.placeholder]="f.placeholder"
|
|
1613
|
+
(input)="setValue(f.key, $any($event.target).value)"
|
|
1614
|
+
/>
|
|
1615
|
+
}
|
|
1616
|
+
@for (error of fieldErrorsFor(f.key); track error.code) {
|
|
1617
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1618
|
+
}
|
|
1619
|
+
</label>
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1624
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.emailLabel') }}</span>
|
|
1625
|
+
<input
|
|
1626
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1627
|
+
type="email"
|
|
1628
|
+
name="email"
|
|
1629
|
+
autocomplete="email"
|
|
1630
|
+
required
|
|
1631
|
+
[value]="values['email'] ?? ''"
|
|
1632
|
+
(input)="setValue('email', $any($event.target).value)"
|
|
1633
|
+
/>
|
|
1634
|
+
@for (error of fieldErrorsFor('email'); track error.code) {
|
|
1635
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1636
|
+
}
|
|
1637
|
+
</label>
|
|
1638
|
+
|
|
1639
|
+
<label [class]="cls('formField', 'atlas-field')">
|
|
1640
|
+
<span [class]="cls('formFieldLabel', 'atlas-field-label')">{{ t('signUp.passwordLabel') }}</span>
|
|
1641
|
+
<input
|
|
1642
|
+
[class]="cls('formFieldInput', 'atlas-input')"
|
|
1643
|
+
type="password"
|
|
1644
|
+
name="password"
|
|
1645
|
+
autocomplete="new-password"
|
|
1646
|
+
required
|
|
1647
|
+
[value]="values['password'] ?? ''"
|
|
1648
|
+
(input)="setValue('password', $any($event.target).value)"
|
|
1649
|
+
/>
|
|
1650
|
+
@for (error of fieldErrorsFor('password'); track error.code) {
|
|
1651
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1652
|
+
}
|
|
1653
|
+
</label>
|
|
1654
|
+
|
|
1655
|
+
@if (showLegal()) {
|
|
1656
|
+
<label [class]="cls('formField', 'atlas-consent')">
|
|
1657
|
+
@if (settings?.legal?.required) {
|
|
1658
|
+
<input
|
|
1659
|
+
type="checkbox"
|
|
1660
|
+
name="consent"
|
|
1661
|
+
[checked]="agreed"
|
|
1662
|
+
(change)="agreed = $any($event.target).checked"
|
|
1663
|
+
/>
|
|
1664
|
+
}
|
|
1665
|
+
<span class="atlas-text">
|
|
1666
|
+
{{ t(settings?.legal?.required ? 'signUp.consentAgree' : 'signUp.consentNotice') }}
|
|
1667
|
+
@if (settings?.legal?.termsUrl; as terms) {
|
|
1668
|
+
<a class="atlas-link" [href]="terms" target="_blank" rel="noopener noreferrer">{{ t('signUp.terms') }}</a>
|
|
1669
|
+
}
|
|
1670
|
+
@if (settings?.legal?.termsUrl && settings?.legal?.privacyUrl) {
|
|
1671
|
+
{{ t('signUp.and') }}
|
|
1672
|
+
}
|
|
1673
|
+
@if (settings?.legal?.privacyUrl; as privacy) {
|
|
1674
|
+
<a class="atlas-link" [href]="privacy" target="_blank" rel="noopener noreferrer">{{ t('signUp.privacy') }}</a>
|
|
1675
|
+
}.
|
|
1676
|
+
</span>
|
|
1677
|
+
@for (error of fieldErrorsFor('consent'); track error.code) {
|
|
1678
|
+
<span [class]="cls('formFieldError', 'atlas-error')">{{ error.message }}</span>
|
|
1679
|
+
}
|
|
1680
|
+
</label>
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
@if (captcha) {
|
|
1684
|
+
<div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
<button
|
|
1688
|
+
[class]="cls('formButtonPrimary', 'atlas-button')"
|
|
1689
|
+
[disabled]="busy || !consentOk()"
|
|
1690
|
+
type="submit"
|
|
1691
|
+
>
|
|
1692
|
+
{{ t('signUp.submit') }}
|
|
1693
|
+
</button>
|
|
1694
|
+
</form>
|
|
1695
|
+
</div>
|
|
1696
|
+
`,
|
|
1697
|
+
}]
|
|
1698
|
+
}], propDecorators: { settings: [{
|
|
1699
|
+
type: Input
|
|
1700
|
+
}], busy: [{
|
|
1701
|
+
type: Input
|
|
1702
|
+
}], errors: [{
|
|
1703
|
+
type: Input
|
|
1704
|
+
}], captcha: [{
|
|
1705
|
+
type: Input
|
|
1706
|
+
}], submitSignUp: [{
|
|
1707
|
+
type: Output
|
|
1708
|
+
}] } });
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* `<atlas-sign-up>` — creates an account. The Angular peer of React's
|
|
1712
|
+
* `<SignUp/>`. The first step is the create-account form (the instance's
|
|
1713
|
+
* configured fields, email, password, legal consent and — when required — the
|
|
1714
|
+
* captcha), which POSTs `/v1/client/sign_ups`; from there it shares the flow
|
|
1715
|
+
* engine with `<atlas-sign-in>` (the emailed-code step, then the ticket
|
|
1716
|
+
* exchange), because the sequence of steps is decided by the server.
|
|
1144
1717
|
*
|
|
1145
1718
|
* ```html
|
|
1146
1719
|
* <atlas-sign-up *atlasSignedOut></atlas-sign-up>
|
|
@@ -1150,20 +1723,35 @@ class SignUpComponent {
|
|
|
1150
1723
|
atlas = inject(AtlasService);
|
|
1151
1724
|
/** Where to send the user once the flow completes. Reserved for host routing. */
|
|
1152
1725
|
afterUrl;
|
|
1153
|
-
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt);
|
|
1726
|
+
flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(), this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
|
|
1727
|
+
ngOnInit() {
|
|
1728
|
+
// §5 the instance's sign-up fields, consent policy and captcha settings.
|
|
1729
|
+
if (typeof window !== 'undefined')
|
|
1730
|
+
void this.flow.loadSettings();
|
|
1731
|
+
}
|
|
1154
1732
|
ngOnDestroy() {
|
|
1155
1733
|
this.flow.destroy();
|
|
1156
1734
|
}
|
|
1157
1735
|
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
|
-
|
|
1736
|
+
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: `
|
|
1737
|
+
@if (flow.state().attempt) {
|
|
1738
|
+
<atlas-attempt-flow
|
|
1739
|
+
[attempt]="flow.state().attempt"
|
|
1740
|
+
[busy]="flow.state().busy"
|
|
1741
|
+
[errors]="flow.state().errors"
|
|
1742
|
+
titleKey="signUp.title"
|
|
1743
|
+
(submitFlow)="flow.submit($event)"
|
|
1744
|
+
></atlas-attempt-flow>
|
|
1745
|
+
} @else {
|
|
1746
|
+
<atlas-sign-up-form
|
|
1747
|
+
[settings]="flow.settings()"
|
|
1748
|
+
[busy]="flow.state().busy"
|
|
1749
|
+
[errors]="flow.state().errors"
|
|
1750
|
+
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
1751
|
+
(submitSignUp)="flow.create($event.values, $event.consent)"
|
|
1752
|
+
></atlas-sign-up-form>
|
|
1753
|
+
}
|
|
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 });
|
|
1167
1755
|
}
|
|
1168
1756
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
|
|
1169
1757
|
type: Component,
|
|
@@ -1171,15 +1759,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1171
1759
|
selector: 'atlas-sign-up',
|
|
1172
1760
|
standalone: true,
|
|
1173
1761
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1174
|
-
imports: [AttemptFlowComponent],
|
|
1762
|
+
imports: [AttemptFlowComponent, SignUpFormComponent],
|
|
1175
1763
|
template: `
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1764
|
+
@if (flow.state().attempt) {
|
|
1765
|
+
<atlas-attempt-flow
|
|
1766
|
+
[attempt]="flow.state().attempt"
|
|
1767
|
+
[busy]="flow.state().busy"
|
|
1768
|
+
[errors]="flow.state().errors"
|
|
1769
|
+
titleKey="signUp.title"
|
|
1770
|
+
(submitFlow)="flow.submit($event)"
|
|
1771
|
+
></atlas-attempt-flow>
|
|
1772
|
+
} @else {
|
|
1773
|
+
<atlas-sign-up-form
|
|
1774
|
+
[settings]="flow.settings()"
|
|
1775
|
+
[busy]="flow.state().busy"
|
|
1776
|
+
[errors]="flow.state().errors"
|
|
1777
|
+
[captcha]="flow.signUpCaptchaDrawn() ? flow.signUpCaptcha : null"
|
|
1778
|
+
(submitSignUp)="flow.create($event.values, $event.consent)"
|
|
1779
|
+
></atlas-sign-up-form>
|
|
1780
|
+
}
|
|
1183
1781
|
`,
|
|
1184
1782
|
}]
|
|
1185
1783
|
}], propDecorators: { afterUrl: [{
|
|
@@ -1630,5 +2228,5 @@ function atlasChildGuard(options = {}) {
|
|
|
1630
2228
|
* Generated bundle index. Do not edit.
|
|
1631
2229
|
*/
|
|
1632
2230
|
|
|
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 };
|
|
2231
|
+
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
2232
|
//# sourceMappingURL=atlasauth-angular.mjs.map
|