@atlasauth/angular 0.2.0 → 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.
@@ -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, readRedirectResult, stripRedirectParams, readExpiry, msUntilRefresh, nextStep, flowFromPending, initialFlow, advance, shouldKeepPolling, pollAttempt } from '@atlasauth/js';
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
- this.baseUrl = options.frontendApi ?? '';
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({
@@ -397,12 +413,16 @@ class AtlasService {
397
413
  return evaluate(this._claims.value, condition).allowed;
398
414
  }
399
415
  /** Revoke the current session server-side and drop all local auth state. */
400
- async signOut() {
416
+ async signOut(options) {
401
417
  const current = this.cache.peek();
402
418
  if (current) {
403
- await this.call(`/v1/client/sessions/${current.sessionId}/revoke`, {
419
+ const revoked = await this.call(`/v1/client/sessions/${current.sessionId}/revoke`, {
404
420
  method: 'POST',
405
421
  }).catch(() => undefined);
422
+ // OIDC Front-Channel Logout: let the apps signed in through this session
423
+ // clear theirs in this browser (hidden iframes, capped at 5 s). Best-effort.
424
+ if (options?.frontchannel !== false)
425
+ await runFrontChannelLogoutFromResponse(revoked);
406
426
  }
407
427
  this.clearRefreshTimer();
408
428
  this.cache.clear();
@@ -819,6 +839,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
819
839
  }]
820
840
  }] });
821
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
+
822
882
  /**
823
883
  * The server-driven flow shell, shared by `<atlas-sign-in>` and
824
884
  * `<atlas-sign-up>`. The Angular peer of React's internal `AttemptFlow`.
@@ -835,6 +895,12 @@ class AttemptFlowComponent {
835
895
  busy = false;
836
896
  errors = [];
837
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;
838
904
  /** Emits the collected field values when the form is submitted. */
839
905
  submitFlow = new EventEmitter();
840
906
  values = {};
@@ -860,6 +926,10 @@ class AttemptFlowComponent {
860
926
  step() {
861
927
  return this.attempt ? nextStep(this.attempt) : { kind: 'collect_identifier' };
862
928
  }
929
+ showCaptcha() {
930
+ const kind = this.step().kind;
931
+ return !!this.captcha && (kind === 'collect_identifier' || kind === 'collect_captcha');
932
+ }
863
933
  showSubmit() {
864
934
  const kind = this.step().kind;
865
935
  return kind !== 'done' && kind !== 'await_oauth';
@@ -884,6 +954,10 @@ class AttemptFlowComponent {
884
954
  return [{ kind: 'input', name: 'code', labelKey: 'signIn.emailCodeLabel', type: 'text' }];
885
955
  case 'collect_new_password':
886
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')];
887
961
  case 'await_oauth':
888
962
  return [msg('signIn.magicLinkSent')];
889
963
  case 'done':
@@ -897,7 +971,7 @@ class AttemptFlowComponent {
897
971
  }
898
972
  }
899
973
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
900
- 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: `
901
975
  <div [class]="cls('card', 'atlas-card')">
902
976
  <h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
903
977
 
@@ -913,6 +987,7 @@ class AttemptFlowComponent {
913
987
  <input
914
988
  [class]="cls('formFieldInput', 'atlas-input')"
915
989
  [type]="f.type"
990
+ [attr.name]="f.name"
916
991
  [value]="values[f.name] ?? ''"
917
992
  (input)="setValue(f.name, $any($event.target).value)"
918
993
  />
@@ -925,6 +1000,10 @@ class AttemptFlowComponent {
925
1000
  }
926
1001
  }
927
1002
 
1003
+ @if (showCaptcha()) {
1004
+ <div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
1005
+ }
1006
+
928
1007
  @if (showSubmit()) {
929
1008
  <button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
930
1009
  {{ t('signIn.submit') }}
@@ -932,7 +1011,7 @@ class AttemptFlowComponent {
932
1011
  }
933
1012
  </form>
934
1013
  </div>
935
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1014
+ `, isInline: true, dependencies: [{ kind: "directive", type: AtlasCaptchaSlotDirective, selector: "[atlasCaptchaSlot]", inputs: ["atlasCaptchaSlot"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
936
1015
  }
937
1016
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AttemptFlowComponent, decorators: [{
938
1017
  type: Component,
@@ -940,6 +1019,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
940
1019
  selector: 'atlas-attempt-flow',
941
1020
  standalone: true,
942
1021
  changeDetection: ChangeDetectionStrategy.OnPush,
1022
+ imports: [AtlasCaptchaSlotDirective],
943
1023
  template: `
944
1024
  <div [class]="cls('card', 'atlas-card')">
945
1025
  <h1 [class]="cls('headerTitle', 'atlas-title')">{{ t(titleKey) }}</h1>
@@ -956,6 +1036,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
956
1036
  <input
957
1037
  [class]="cls('formFieldInput', 'atlas-input')"
958
1038
  [type]="f.type"
1039
+ [attr.name]="f.name"
959
1040
  [value]="values[f.name] ?? ''"
960
1041
  (input)="setValue(f.name, $any($event.target).value)"
961
1042
  />
@@ -968,6 +1049,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
968
1049
  }
969
1050
  }
970
1051
 
1052
+ @if (showCaptcha()) {
1053
+ <div class="atlas-captcha" data-atlas-captcha="" [atlasCaptchaSlot]="captcha"></div>
1054
+ }
1055
+
971
1056
  @if (showSubmit()) {
972
1057
  <button [class]="cls('formButtonPrimary', 'atlas-button')" [disabled]="busy" type="submit">
973
1058
  {{ t('signIn.submit') }}
@@ -985,6 +1070,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
985
1070
  type: Input
986
1071
  }], titleKey: [{
987
1072
  type: Input
1073
+ }], captcha: [{
1074
+ type: Input
988
1075
  }], submitFlow: [{
989
1076
  type: Output
990
1077
  }] } });
@@ -1001,18 +1088,68 @@ function msg(labelKey) {
1001
1088
  * server via `advance`, and this controller only posts whatever the current step
1002
1089
  * needs and re-renders the returned status. State lives in a signal so templates
1003
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.
1004
1096
  */
1005
1097
  class AtlasFlowController {
1006
1098
  client;
1007
1099
  reload;
1100
+ t;
1008
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);
1009
1116
  pollTimer = null;
1010
- constructor(client, reload, pending) {
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)) {
1011
1121
  this.client = client;
1012
1122
  this.reload = reload;
1123
+ this.t = t;
1013
1124
  // Seeded from a redirect that came back mid-flow (e.g. OAuth needing a second
1014
1125
  // factor) so the user resumes at the demanded step instead of starting over.
1015
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();
1016
1153
  }
1017
1154
  /**
1018
1155
  * Advance the flow with the values the user typed. On completion, exchange the
@@ -1022,8 +1159,31 @@ class AtlasFlowController {
1022
1159
  */
1023
1160
  async submit(values) {
1024
1161
  this.state.update((current) => ({ ...current, busy: true, errors: [] }));
1025
- const next = await advance(this.client, this.state(), values);
1026
- this.state.set(next);
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);
1027
1187
  if (next.attempt?.status === 'complete') {
1028
1188
  if (next.ticket) {
1029
1189
  await this.client.post('/v1/client/tickets/exchange', {
@@ -1053,7 +1213,7 @@ class AtlasFlowController {
1053
1213
  this.pollTimer = setInterval(() => {
1054
1214
  void (async () => {
1055
1215
  const { state: next, ticket } = await pollAttempt(this.client, this.state());
1056
- this.state.set(next);
1216
+ this.setState(next);
1057
1217
  if (!ticket || !next.attempt)
1058
1218
  return;
1059
1219
  // The ticket is exchanged by a normal request, so no session token ever
@@ -1073,15 +1233,19 @@ class AtlasFlowController {
1073
1233
  this.pollTimer = null;
1074
1234
  }
1075
1235
  }
1076
- /** 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`. */
1077
1237
  destroy() {
1078
1238
  this.stopPolling();
1239
+ this.captcha.destroy();
1240
+ this.signUpCaptcha.destroy();
1079
1241
  }
1080
1242
  }
1081
1243
 
1082
1244
  /**
1083
1245
  * `<atlas-sign-in>` — the full, multi-step sign-in flow driven entirely by the
1084
- * 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).
1085
1249
  *
1086
1250
  * ```html
1087
1251
  * <atlas-sign-in *atlasSignedOut afterUrl="/dashboard"></atlas-sign-in>
@@ -1093,10 +1257,14 @@ class SignInComponent {
1093
1257
  afterUrl;
1094
1258
  flow = new AtlasFlowController(this.atlas.createFapiClient(), () => this.atlas.reload(),
1095
1259
  // Resume mid-flow if an OAuth redirect came back owing a second factor.
1096
- this.atlas.pendingAttempt);
1260
+ this.atlas.pendingAttempt, (key) => translate(this.atlas.localization, key));
1097
1261
  ngOnInit() {
1098
1262
  // If seeded from a pending attempt that is already pollable, start polling.
1099
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();
1100
1268
  }
1101
1269
  ngOnDestroy() {
1102
1270
  this.flow.destroy();
@@ -1108,9 +1276,10 @@ class SignInComponent {
1108
1276
  [busy]="flow.state().busy"
1109
1277
  [errors]="flow.state().errors"
1110
1278
  titleKey="signIn.title"
1279
+ [captcha]="flow.captchaDrawn() ? flow.captcha : null"
1111
1280
  (submitFlow)="flow.submit($event)"
1112
1281
  ></atlas-attempt-flow>
1113
- `, 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 });
1114
1283
  }
1115
1284
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignInComponent, decorators: [{
1116
1285
  type: Component,
@@ -1125,6 +1294,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1125
1294
  [busy]="flow.state().busy"
1126
1295
  [errors]="flow.state().errors"
1127
1296
  titleKey="signIn.title"
1297
+ [captcha]="flow.captchaDrawn() ? flow.captcha : null"
1128
1298
  (submitFlow)="flow.submit($event)"
1129
1299
  ></atlas-attempt-flow>
1130
1300
  `,
@@ -1134,9 +1304,416 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1134
1304
  }] } });
1135
1305
 
1136
1306
  /**
1137
- * `<atlas-sign-up>` — the full, server-driven sign-up flow. The Angular peer of
1138
- * React's `<SignUp/>`. It shares the exact flow engine with `<atlas-sign-in>`;
1139
- * only the title differs, because the sequence of steps is decided by the server.
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.
1140
1717
  *
1141
1718
  * ```html
1142
1719
  * <atlas-sign-up *atlasSignedOut></atlas-sign-up>
@@ -1146,20 +1723,35 @@ class SignUpComponent {
1146
1723
  atlas = inject(AtlasService);
1147
1724
  /** Where to send the user once the flow completes. Reserved for host routing. */
1148
1725
  afterUrl;
1149
- 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
+ }
1150
1732
  ngOnDestroy() {
1151
1733
  this.flow.destroy();
1152
1734
  }
1153
1735
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1154
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SignUpComponent, isStandalone: true, selector: "atlas-sign-up", inputs: { afterUrl: "afterUrl" }, ngImport: i0, template: `
1155
- <atlas-attempt-flow
1156
- [attempt]="flow.state().attempt"
1157
- [busy]="flow.state().busy"
1158
- [errors]="flow.state().errors"
1159
- titleKey="signUp.title"
1160
- (submitFlow)="flow.submit($event)"
1161
- ></atlas-attempt-flow>
1162
- `, isInline: true, dependencies: [{ kind: "component", type: AttemptFlowComponent, selector: "atlas-attempt-flow", inputs: ["attempt", "busy", "errors", "titleKey"], outputs: ["submitFlow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
1163
1755
  }
1164
1756
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SignUpComponent, decorators: [{
1165
1757
  type: Component,
@@ -1167,15 +1759,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1167
1759
  selector: 'atlas-sign-up',
1168
1760
  standalone: true,
1169
1761
  changeDetection: ChangeDetectionStrategy.OnPush,
1170
- imports: [AttemptFlowComponent],
1762
+ imports: [AttemptFlowComponent, SignUpFormComponent],
1171
1763
  template: `
1172
- <atlas-attempt-flow
1173
- [attempt]="flow.state().attempt"
1174
- [busy]="flow.state().busy"
1175
- [errors]="flow.state().errors"
1176
- titleKey="signUp.title"
1177
- (submitFlow)="flow.submit($event)"
1178
- ></atlas-attempt-flow>
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
+ }
1179
1781
  `,
1180
1782
  }]
1181
1783
  }], propDecorators: { afterUrl: [{
@@ -1626,5 +2228,5 @@ function atlasChildGuard(options = {}) {
1626
2228
  * Generated bundle index. Do not edit.
1627
2229
  */
1628
2230
 
1629
- 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 };
1630
2232
  //# sourceMappingURL=atlasauth-angular.mjs.map