valtech-components 2.0.834 → 2.0.836

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.
@@ -53,7 +53,7 @@ import 'prismjs/components/prism-json';
53
53
  * Current version of valtech-components.
54
54
  * This is automatically updated during the publish process.
55
55
  */
56
- const VERSION = '2.0.834';
56
+ const VERSION = '2.0.836';
57
57
 
58
58
  /**
59
59
  * Servicio para gestionar presets de componentes.
@@ -4352,6 +4352,60 @@ const VALTECH_DEFAULT_CONTENT = {
4352
4352
  setPasswordConfirmCancel: 'Cancelar',
4353
4353
  passwordSetSuccess: '¡Contraseña creada!',
4354
4354
  errorPasswordAlreadySet: 'Esta cuenta ya tiene una contraseña.',
4355
+ // MFA — autenticación en dos pasos
4356
+ mfaManageTitle: 'Autenticación en dos pasos',
4357
+ mfaEnabledLabel: 'MFA habilitado',
4358
+ mfaDisabledLabel: 'MFA deshabilitado',
4359
+ mfaDisabledHint: 'Habilita MFA para mayor seguridad en tu cuenta.',
4360
+ mfaEnableButton: 'Habilitar MFA',
4361
+ mfaDisableButton: 'Deshabilitar MFA',
4362
+ mfaEnableTitle: 'Habilitar MFA',
4363
+ mfaMethodPrompt: 'Elige el método de verificación:',
4364
+ mfaMethodTotp: 'App de autenticación',
4365
+ mfaMethodTotpHint: 'Usa Google Authenticator u otra app (recomendado).',
4366
+ mfaMethodEmail: 'Correo electrónico',
4367
+ mfaMethodEmailHint: 'Recibe códigos en tu correo.',
4368
+ mfaMethodSms: 'SMS',
4369
+ mfaMethodSmsHint: 'Recibe códigos por mensaje de texto.',
4370
+ mfaPhoneLabel: 'Teléfono',
4371
+ mfaPhoneInvalid: 'Ingresa un número válido en formato E.164 (ej: +56912345678).',
4372
+ mfaPhoneRegistered: 'Teléfono registrado',
4373
+ mfaContinue: 'Continuar',
4374
+ mfaCancel: 'Cancelar',
4375
+ mfaTotpSetupTitle: 'Configurar app de autenticación',
4376
+ mfaTotpStep1: 'Paso 1 — Escanea el código QR con tu app de autenticación.',
4377
+ mfaTotpManualEntry: '¿No puedes escanear? Ingresa este código manualmente:',
4378
+ mfaTotpStep2: 'Paso 2 — Ingresa el código de 6 dígitos de tu app:',
4379
+ mfaTotpVerify: 'Verificar y activar',
4380
+ mfaConfirmTitle: 'Confirmar MFA',
4381
+ mfaConfirmPromptEmail: 'Ingresa el código de 6 dígitos enviado a tu correo.',
4382
+ mfaConfirmPromptSms: 'Ingresa el código de 6 dígitos enviado a tu teléfono.',
4383
+ mfaConfirmButton: 'Confirmar',
4384
+ mfaNoCode: '¿No recibiste el código?',
4385
+ mfaResend: 'Reenviar',
4386
+ mfaResendIn: 'Reenviar en',
4387
+ mfaBackupCodesTitle: 'Códigos de respaldo',
4388
+ mfaBackupCodesAvailable: 'Códigos disponibles',
4389
+ mfaBackupCodesLow: 'Te quedan pocos códigos. Considera regenerarlos.',
4390
+ mfaBackupCodesSaveWarning: 'Importante: guarda estos códigos de respaldo.',
4391
+ mfaBackupCodesExplain: 'Si pierdes acceso a tu app, estos códigos te permiten entrar. Cada uno se usa una sola vez.',
4392
+ mfaCopyCodes: 'Copiar códigos',
4393
+ mfaRegenerateCodes: 'Regenerar códigos de respaldo',
4394
+ mfaDisableTitle: 'Deshabilitar MFA',
4395
+ mfaDisablePrompt: 'Ingresa tu contraseña para deshabilitar MFA.',
4396
+ mfaPasswordLabel: 'Contraseña',
4397
+ mfaCodeInvalid: 'Ingresa un código válido de 6 dígitos.',
4398
+ mfaPasswordRequired: 'Ingresa tu contraseña.',
4399
+ mfaEnabledOk: '¡MFA habilitado correctamente!',
4400
+ mfaDisabledOk: 'MFA deshabilitado correctamente.',
4401
+ mfaCodesCopied: 'Códigos copiados al portapapeles.',
4402
+ mfaErrorInvalidCode: 'Código incorrecto.',
4403
+ mfaErrorExpiredCode: 'El código ha expirado. Solicita uno nuevo.',
4404
+ mfaErrorCodeUsed: 'Este código ya fue utilizado.',
4405
+ mfaErrorAlreadyActive: 'MFA ya está habilitado.',
4406
+ mfaErrorNotEnabled: 'MFA no está habilitado.',
4407
+ mfaErrorPhoneRequired: 'Número de teléfono requerido.',
4408
+ mfaErrorPhoneExists: 'Este teléfono ya está en uso.',
4355
4409
  // Legal
4356
4410
  legalPrefix: 'Utilizamos los servicios de',
4357
4411
  legalSuffix: 'para ofrecerte una experiencia segura. Al iniciar sesión, aceptas nuestros',
@@ -4452,6 +4506,60 @@ const VALTECH_DEFAULT_CONTENT = {
4452
4506
  setPasswordConfirmCancel: 'Cancel',
4453
4507
  passwordSetSuccess: 'Password created!',
4454
4508
  errorPasswordAlreadySet: 'This account already has a password.',
4509
+ // MFA — two-factor authentication
4510
+ mfaManageTitle: 'Two-factor authentication',
4511
+ mfaEnabledLabel: 'MFA enabled',
4512
+ mfaDisabledLabel: 'MFA disabled',
4513
+ mfaDisabledHint: 'Enable MFA for extra account security.',
4514
+ mfaEnableButton: 'Enable MFA',
4515
+ mfaDisableButton: 'Disable MFA',
4516
+ mfaEnableTitle: 'Enable MFA',
4517
+ mfaMethodPrompt: 'Choose your verification method:',
4518
+ mfaMethodTotp: 'Authenticator app',
4519
+ mfaMethodTotpHint: 'Use Google Authenticator or another app (recommended).',
4520
+ mfaMethodEmail: 'Email',
4521
+ mfaMethodEmailHint: 'Receive codes in your email.',
4522
+ mfaMethodSms: 'SMS',
4523
+ mfaMethodSmsHint: 'Receive codes by text message.',
4524
+ mfaPhoneLabel: 'Phone',
4525
+ mfaPhoneInvalid: 'Enter a valid number in E.164 format (e.g. +56912345678).',
4526
+ mfaPhoneRegistered: 'Registered phone',
4527
+ mfaContinue: 'Continue',
4528
+ mfaCancel: 'Cancel',
4529
+ mfaTotpSetupTitle: 'Set up authenticator app',
4530
+ mfaTotpStep1: 'Step 1 — Scan the QR code with your authenticator app.',
4531
+ mfaTotpManualEntry: "Can't scan? Enter this code manually:",
4532
+ mfaTotpStep2: 'Step 2 — Enter the 6-digit code from your app:',
4533
+ mfaTotpVerify: 'Verify and activate',
4534
+ mfaConfirmTitle: 'Confirm MFA',
4535
+ mfaConfirmPromptEmail: 'Enter the 6-digit code sent to your email.',
4536
+ mfaConfirmPromptSms: 'Enter the 6-digit code sent to your phone.',
4537
+ mfaConfirmButton: 'Confirm',
4538
+ mfaNoCode: "Didn't receive the code?",
4539
+ mfaResend: 'Resend',
4540
+ mfaResendIn: 'Resend in',
4541
+ mfaBackupCodesTitle: 'Backup codes',
4542
+ mfaBackupCodesAvailable: 'Available codes',
4543
+ mfaBackupCodesLow: 'You have few codes left. Consider regenerating them.',
4544
+ mfaBackupCodesSaveWarning: 'Important: save these backup codes.',
4545
+ mfaBackupCodesExplain: 'If you lose access to your app, these codes let you sign in. Each can be used once.',
4546
+ mfaCopyCodes: 'Copy codes',
4547
+ mfaRegenerateCodes: 'Regenerate backup codes',
4548
+ mfaDisableTitle: 'Disable MFA',
4549
+ mfaDisablePrompt: 'Enter your password to disable MFA.',
4550
+ mfaPasswordLabel: 'Password',
4551
+ mfaCodeInvalid: 'Enter a valid 6-digit code.',
4552
+ mfaPasswordRequired: 'Enter your password.',
4553
+ mfaEnabledOk: 'MFA enabled successfully!',
4554
+ mfaDisabledOk: 'MFA disabled successfully.',
4555
+ mfaCodesCopied: 'Codes copied to clipboard.',
4556
+ mfaErrorInvalidCode: 'Incorrect code.',
4557
+ mfaErrorExpiredCode: 'The code has expired. Request a new one.',
4558
+ mfaErrorCodeUsed: 'This code was already used.',
4559
+ mfaErrorAlreadyActive: 'MFA is already enabled.',
4560
+ mfaErrorNotEnabled: 'MFA is not enabled.',
4561
+ mfaErrorPhoneRequired: 'Phone number required.',
4562
+ mfaErrorPhoneExists: 'This phone is already in use.',
4455
4563
  // Legal
4456
4564
  legalPrefix: 'We use the services of',
4457
4565
  legalSuffix: 'to offer you a secure experience. By signing in, you accept our',
@@ -24789,13 +24897,16 @@ class AuthService {
24789
24897
  signinWithOAuth(provider) {
24790
24898
  this.stateService.clearError();
24791
24899
  return this.oauthService.startFlow(provider).pipe(tap(result => {
24792
- // DEBUG: Log OAuth result to check firebaseToken
24793
- console.log('[ValtechAuth] OAuth result received:', {
24794
- hasAccessToken: !!result.accessToken,
24795
- hasRefreshToken: !!result.refreshToken,
24796
- hasFirebaseToken: !!result.firebaseToken,
24797
- firebaseTokenLength: result.firebaseToken?.length || 0,
24798
- });
24900
+ // MFA requerido tras OAuth guardar estado temporal, NO autenticar.
24901
+ // El login component reacciona a `mfaPending()` y abre el modal verify.
24902
+ if (result.mfaRequired) {
24903
+ this.stateService.setMFAPending({
24904
+ required: true,
24905
+ mfaToken: result.mfaToken,
24906
+ method: result.mfaMethod,
24907
+ });
24908
+ return;
24909
+ }
24799
24910
  // Convertir OAuthResult a SigninResponse compatible
24800
24911
  const response = {
24801
24912
  operationId: 'oauth',
@@ -24807,15 +24918,22 @@ class AuthService {
24807
24918
  permissions: result.permissions,
24808
24919
  };
24809
24920
  this.handleSuccessfulAuth(response);
24810
- }), map$1(result => ({
24811
- operationId: 'oauth',
24812
- accessToken: result.accessToken,
24813
- refreshToken: result.refreshToken,
24814
- firebaseToken: result.firebaseToken,
24815
- expiresIn: result.expiresIn,
24816
- roles: result.roles,
24817
- permissions: result.permissions,
24818
- })), catchError(error => {
24921
+ }), map$1(result => result.mfaRequired
24922
+ ? {
24923
+ operationId: 'oauth',
24924
+ mfaRequired: true,
24925
+ mfaToken: result.mfaToken,
24926
+ mfaMethod: result.mfaMethod,
24927
+ }
24928
+ : {
24929
+ operationId: 'oauth',
24930
+ accessToken: result.accessToken,
24931
+ refreshToken: result.refreshToken,
24932
+ firebaseToken: result.firebaseToken,
24933
+ expiresIn: result.expiresIn,
24934
+ roles: result.roles,
24935
+ permissions: result.permissions,
24936
+ }), catchError(error => {
24819
24937
  const authError = {
24820
24938
  code: error.code || 'OAUTH_ERROR',
24821
24939
  message: error.message || 'Error de autenticación OAuth',
@@ -28330,6 +28448,24 @@ class OAuthCallbackComponent {
28330
28448
  this.closeAfterDelay();
28331
28449
  return;
28332
28450
  }
28451
+ // MFA requerido — el backend redirige sin tokens, con mfa_token. El flujo
28452
+ // continúa con el challenge MFA (AuthService.setMFAPending → modal verify).
28453
+ if (params.get('mfa_required') === 'true') {
28454
+ this.sendToParent({
28455
+ type: 'oauth-callback',
28456
+ tokens: {
28457
+ accessToken: '',
28458
+ refreshToken: '',
28459
+ expiresIn: 0,
28460
+ mfaRequired: true,
28461
+ mfaToken: params.get('mfa_token') || undefined,
28462
+ mfaMethod: params.get('mfa_method') || undefined,
28463
+ },
28464
+ });
28465
+ this.message = 'Verificación adicional requerida';
28466
+ this.closeAfterDelay();
28467
+ return;
28468
+ }
28333
28469
  // Extraer tokens
28334
28470
  const accessToken = params.get('access_token');
28335
28471
  const refreshToken = params.get('refresh_token');
@@ -30126,6 +30262,455 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
30126
30262
  type: Output
30127
30263
  }] } });
30128
30264
 
30265
+ /** Segundos de espera antes de poder reenviar el código EMAIL/SMS. */
30266
+ const RESEND_COOLDOWN_SECONDS = 30;
30267
+ /**
30268
+ * `val-mfa-modal` — modal de gestión de autenticación de dos factores (MFA)
30269
+ * para un usuario autenticado. Mismo patrón que `val-change-password-modal`.
30270
+ *
30271
+ * Flujo (máquina de estados interna):
30272
+ * - `loading` → `getProfile()` para conocer el estado MFA.
30273
+ * - `status` → muestra MFA habilitado/deshabilitado. Si está habilitado:
30274
+ * gestión de backup codes (TOTP) + deshabilitar. Si no: botón habilitar.
30275
+ * - `method-select` → elegir TOTP / EMAIL / SMS.
30276
+ * - `totp-setup` → QR + secreto manual + backup codes → verificar código.
30277
+ * - `code-confirm` → (EMAIL/SMS) ingresar código recibido, con reenvío.
30278
+ * - `disable` → contraseña para deshabilitar MFA.
30279
+ *
30280
+ * El QR se genera **client-side** (`QrGeneratorService`) — el secreto TOTP
30281
+ * nunca sale del navegador.
30282
+ *
30283
+ * Self-contained: inyecta `AuthService` y llama los endpoints directo. La app
30284
+ * controla `[isOpen]` y reacciona a `(changed)` / `(dismissed)`.
30285
+ *
30286
+ * i18n: namespace compartido `_auth`.
30287
+ *
30288
+ * @example
30289
+ * ```html
30290
+ * <val-mfa-modal
30291
+ * [isOpen]="isModalOpen()"
30292
+ * (dismissed)="isModalOpen.set(false)"
30293
+ * />
30294
+ * ```
30295
+ */
30296
+ class MfaModalComponent {
30297
+ /** Controla la visibilidad. Cada apertura re-resuelve el estado MFA. */
30298
+ set isOpen(value) {
30299
+ const opening = value && !this._isOpen;
30300
+ this._isOpen = value;
30301
+ if (opening) {
30302
+ this.open();
30303
+ }
30304
+ }
30305
+ get isOpen() {
30306
+ return this._isOpen;
30307
+ }
30308
+ constructor() {
30309
+ this._isOpen = false;
30310
+ /** Emite cuando el estado MFA cambia (habilitado / deshabilitado). */
30311
+ this.changed = new EventEmitter();
30312
+ /** Emite cuando el user cierra el modal (botón X o backdrop). */
30313
+ this.dismissed = new EventEmitter();
30314
+ this.auth = inject(AuthService);
30315
+ this.toast = inject(ToastService);
30316
+ this.i18n = inject(I18nService);
30317
+ this.i18nHelper = inject(InputI18nHelper);
30318
+ this.qrGen = inject(QrGeneratorService);
30319
+ this._step = signal('loading');
30320
+ /** Paso actual del flujo. */
30321
+ this.step = this._step.asReadonly();
30322
+ /** `true` mientras una llamada al backend está en curso. */
30323
+ this.working = signal(false);
30324
+ // Estado MFA actual.
30325
+ this.mfaEnabled = signal(false);
30326
+ this.mfaMethod = signal(null);
30327
+ this.userPhone = signal(null);
30328
+ this.backupCodesCount = signal(0);
30329
+ // Estado del flujo de habilitación.
30330
+ this.selectedMethod = signal('TOTP');
30331
+ this.totpSetup = signal(null);
30332
+ this.totpQr = signal(null);
30333
+ /** Códigos de respaldo recién regenerados — se muestran una sola vez. */
30334
+ this.regeneratedCodes = signal(null);
30335
+ this.resendCooldown = signal(0);
30336
+ this.pinControl = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(6)]);
30337
+ this.phoneControl = new FormControl('', [Validators.required, Validators.pattern(/^\+[1-9]\d{6,14}$/)]);
30338
+ this.pinInputProps = {
30339
+ control: this.pinControl,
30340
+ token: 'mfa-code',
30341
+ length: 6,
30342
+ allowNumbersOnly: true,
30343
+ autoFocus: true,
30344
+ };
30345
+ /** Form de deshabilitación — `val-form` con un campo de contraseña. */
30346
+ this.disableFormProps = computed(() => this.i18nHelper.resolveForm({
30347
+ nameKey: 'mfaDisableTitle',
30348
+ i18nNamespace: '_auth',
30349
+ sections: [
30350
+ {
30351
+ name: this.t('mfaDisablePrompt'),
30352
+ order: 0,
30353
+ fields: [
30354
+ {
30355
+ type: InputType.PASSWORD,
30356
+ name: 'password',
30357
+ token: 'mfa-disable-password',
30358
+ labelKey: 'mfaPasswordLabel',
30359
+ hint: '',
30360
+ placeholderKey: 'passwordPlaceholder',
30361
+ errorKeys: { required: 'mfaPasswordRequired' },
30362
+ validators: [Validators.required],
30363
+ order: 0,
30364
+ state: ComponentStates.ENABLED,
30365
+ },
30366
+ ],
30367
+ },
30368
+ ],
30369
+ actions: {
30370
+ ...SolidDefaultBlock('', 'submit'),
30371
+ token: 'mfa-disable-submit',
30372
+ textKey: 'mfaDisableButton',
30373
+ },
30374
+ state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
30375
+ }));
30376
+ this.resendTimer = null;
30377
+ addIcons({ closeOutline, informationCircleOutline });
30378
+ }
30379
+ ngOnDestroy() {
30380
+ this.stopCooldown();
30381
+ }
30382
+ /** Traduce una clave del namespace `_auth`. */
30383
+ t(key) {
30384
+ return this.i18n.t(key, '_auth');
30385
+ }
30386
+ /** Cierre iniciado por el user (X / backdrop). */
30387
+ close() {
30388
+ this.dismissed.emit();
30389
+ }
30390
+ /**
30391
+ * Punto de entrada al abrir el modal. Con `prefillCode` (deep-link del email
30392
+ * de setup MFA-email) salta directo a la confirmación; si no, resuelve el
30393
+ * estado MFA actual.
30394
+ */
30395
+ open() {
30396
+ if (this.prefillCode) {
30397
+ this.resetFlow();
30398
+ this.selectedMethod.set('EMAIL');
30399
+ this.pinControl.setValue(this.prefillCode);
30400
+ this._step.set('code-confirm');
30401
+ return;
30402
+ }
30403
+ this.resolveStatus();
30404
+ }
30405
+ // ===========================================================================
30406
+ // Carga de estado
30407
+ // ===========================================================================
30408
+ /** Consulta el perfil para conocer el estado MFA y posicionar el flujo. */
30409
+ resolveStatus() {
30410
+ this._step.set('loading');
30411
+ this.resetFlow();
30412
+ this.auth.getProfile().subscribe({
30413
+ next: profile => {
30414
+ this.mfaEnabled.set(profile.mfaEnabled);
30415
+ this.mfaMethod.set(profile.mfaMethod ?? null);
30416
+ this.userPhone.set(profile.phone ?? null);
30417
+ if (profile.mfaEnabled && profile.mfaMethod === 'TOTP') {
30418
+ this.loadBackupCount();
30419
+ }
30420
+ this._step.set('status');
30421
+ },
30422
+ error: () => {
30423
+ // Fallback: usar el signal de usuario en sesión.
30424
+ const user = this.auth.user();
30425
+ this.mfaEnabled.set(user?.mfaEnabled ?? false);
30426
+ this.mfaMethod.set(user?.mfaMethod ?? null);
30427
+ this._step.set('status');
30428
+ },
30429
+ });
30430
+ }
30431
+ loadBackupCount() {
30432
+ this.auth.getBackupCodesCount().subscribe({
30433
+ next: res => this.backupCodesCount.set(res.count),
30434
+ error: () => this.backupCodesCount.set(0),
30435
+ });
30436
+ }
30437
+ // ===========================================================================
30438
+ // Navegación entre pasos
30439
+ // ===========================================================================
30440
+ goToMethodSelect() {
30441
+ this.regeneratedCodes.set(null);
30442
+ this._step.set('method-select');
30443
+ }
30444
+ goToDisable() {
30445
+ this._step.set('disable');
30446
+ }
30447
+ backToStatus() {
30448
+ this.stopCooldown();
30449
+ this.resolveStatus();
30450
+ }
30451
+ // ===========================================================================
30452
+ // Habilitar MFA
30453
+ // ===========================================================================
30454
+ /** Continúa desde el selector de método al setup correspondiente. */
30455
+ proceedWithMethod() {
30456
+ const method = this.selectedMethod();
30457
+ if (method === 'TOTP') {
30458
+ this.setupTotp();
30459
+ return;
30460
+ }
30461
+ let phone;
30462
+ if (method === 'SMS' && !this.userPhone()) {
30463
+ if (this.phoneControl.invalid) {
30464
+ this.phoneControl.markAsTouched();
30465
+ this.showToast(this.t('mfaPhoneInvalid'));
30466
+ return;
30467
+ }
30468
+ phone = this.phoneControl.value ?? undefined;
30469
+ }
30470
+ this.working.set(true);
30471
+ this.auth.setupMFA(method, phone).subscribe({
30472
+ next: res => {
30473
+ this.working.set(false);
30474
+ if (res.codeSent) {
30475
+ this.pinControl.reset();
30476
+ this._step.set('code-confirm');
30477
+ this.startCooldown();
30478
+ }
30479
+ },
30480
+ error: err => {
30481
+ this.working.set(false);
30482
+ this.showToast(this.resolveError(err));
30483
+ },
30484
+ });
30485
+ }
30486
+ setupTotp() {
30487
+ this.working.set(true);
30488
+ this.auth.setupTOTP().subscribe({
30489
+ next: res => {
30490
+ this.totpSetup.set(res);
30491
+ this.pinControl.reset();
30492
+ this.qrGen
30493
+ .generate({ data: res.qrCodeUrl, width: 220 })
30494
+ .then(qr => this.totpQr.set(qr))
30495
+ .catch(() => this.totpQr.set(null));
30496
+ this.working.set(false);
30497
+ this._step.set('totp-setup');
30498
+ },
30499
+ error: err => {
30500
+ this.working.set(false);
30501
+ this.showToast(this.resolveError(err));
30502
+ },
30503
+ });
30504
+ }
30505
+ /** Verifica el código TOTP de la app de autenticación y activa MFA. */
30506
+ verifyTotp() {
30507
+ const code = this.pinControl.value;
30508
+ if (!code || code.length !== 6) {
30509
+ this.showToast(this.t('mfaCodeInvalid'));
30510
+ return;
30511
+ }
30512
+ this.working.set(true);
30513
+ this.auth.verifyTOTPSetup(code).subscribe({
30514
+ next: res => {
30515
+ this.working.set(false);
30516
+ if (res.enabled) {
30517
+ this.showToast(this.t('mfaEnabledOk'));
30518
+ this.changed.emit();
30519
+ this.resolveStatus();
30520
+ }
30521
+ },
30522
+ error: err => {
30523
+ this.working.set(false);
30524
+ this.showToast(this.resolveError(err));
30525
+ },
30526
+ });
30527
+ }
30528
+ /** Confirma el código EMAIL/SMS y activa MFA. */
30529
+ confirmCode() {
30530
+ const code = this.pinControl.value;
30531
+ if (!code || code.length !== 6) {
30532
+ this.showToast(this.t('mfaCodeInvalid'));
30533
+ return;
30534
+ }
30535
+ this.working.set(true);
30536
+ this.auth.confirmMFA(code).subscribe({
30537
+ next: res => {
30538
+ this.working.set(false);
30539
+ if (res.mfaEnabled) {
30540
+ this.showToast(this.t('mfaEnabledOk'));
30541
+ this.changed.emit();
30542
+ this.resolveStatus();
30543
+ }
30544
+ },
30545
+ error: err => {
30546
+ this.working.set(false);
30547
+ this.showToast(this.resolveError(err));
30548
+ },
30549
+ });
30550
+ }
30551
+ /** Reenvía el código EMAIL/SMS (re-ejecuta el setup). */
30552
+ resendCode() {
30553
+ if (this.resendCooldown() > 0) {
30554
+ return;
30555
+ }
30556
+ this.proceedWithMethod();
30557
+ }
30558
+ // ===========================================================================
30559
+ // Gestión (MFA habilitado)
30560
+ // ===========================================================================
30561
+ /** Regenera los códigos de respaldo TOTP y los muestra una vez. */
30562
+ regenerateBackupCodes() {
30563
+ this.working.set(true);
30564
+ this.auth.regenerateBackupCodes().subscribe({
30565
+ next: res => {
30566
+ this.working.set(false);
30567
+ this.backupCodesCount.set(res.backupCodes.length);
30568
+ this.regeneratedCodes.set(res.backupCodes);
30569
+ },
30570
+ error: err => {
30571
+ this.working.set(false);
30572
+ this.showToast(this.resolveError(err));
30573
+ },
30574
+ });
30575
+ }
30576
+ /** Deshabilita MFA — requiere la contraseña de la cuenta. */
30577
+ onDisableSubmit(event) {
30578
+ const password = event.fields['password'];
30579
+ if (!password) {
30580
+ this.showToast(this.t('mfaPasswordRequired'));
30581
+ return;
30582
+ }
30583
+ this.working.set(true);
30584
+ this.auth.disableMFA(password).subscribe({
30585
+ next: res => {
30586
+ this.working.set(false);
30587
+ if (res.mfaDisabled) {
30588
+ this.showToast(this.t('mfaDisabledOk'));
30589
+ this.changed.emit();
30590
+ this.resolveStatus();
30591
+ }
30592
+ },
30593
+ error: err => {
30594
+ this.working.set(false);
30595
+ this.showToast(this.resolveError(err));
30596
+ },
30597
+ });
30598
+ }
30599
+ /** Copia una lista de códigos de respaldo al portapapeles. */
30600
+ async copyCodes(codes) {
30601
+ try {
30602
+ await navigator.clipboard.writeText(codes.join('\n'));
30603
+ this.showToast(this.t('mfaCodesCopied'));
30604
+ }
30605
+ catch {
30606
+ /* sin recurso de copia disponible */
30607
+ }
30608
+ }
30609
+ /** Etiqueta i18n legible para un método MFA. */
30610
+ methodLabel(method) {
30611
+ switch (method) {
30612
+ case 'TOTP':
30613
+ return this.t('mfaMethodTotp');
30614
+ case 'EMAIL':
30615
+ return this.t('mfaMethodEmail');
30616
+ case 'SMS':
30617
+ return this.t('mfaMethodSms');
30618
+ default:
30619
+ return '';
30620
+ }
30621
+ }
30622
+ // ===========================================================================
30623
+ // Helpers
30624
+ // ===========================================================================
30625
+ resetFlow() {
30626
+ this.pinControl.reset();
30627
+ this.phoneControl.reset();
30628
+ this.totpSetup.set(null);
30629
+ this.totpQr.set(null);
30630
+ this.regeneratedCodes.set(null);
30631
+ this.selectedMethod.set('TOTP');
30632
+ this.stopCooldown();
30633
+ }
30634
+ startCooldown() {
30635
+ this.stopCooldown();
30636
+ this.resendCooldown.set(RESEND_COOLDOWN_SECONDS);
30637
+ this.resendTimer = setInterval(() => {
30638
+ this.resendCooldown.update(v => v - 1);
30639
+ if (this.resendCooldown() <= 0) {
30640
+ this.stopCooldown();
30641
+ }
30642
+ }, 1000);
30643
+ }
30644
+ stopCooldown() {
30645
+ if (this.resendTimer) {
30646
+ clearInterval(this.resendTimer);
30647
+ this.resendTimer = null;
30648
+ }
30649
+ this.resendCooldown.set(0);
30650
+ }
30651
+ /** Mapea los códigos de error MFA del backend a mensajes del namespace `_auth`. */
30652
+ resolveError(err) {
30653
+ const code = err?.code;
30654
+ switch (code) {
30655
+ case 'AUTHV2_MFA_INVALID_CODE':
30656
+ return this.t('mfaErrorInvalidCode');
30657
+ case 'AUTHV2_EXPIRED_CODE':
30658
+ return this.t('mfaErrorExpiredCode');
30659
+ case 'AUTHV2_CODE_USED':
30660
+ return this.t('mfaErrorCodeUsed');
30661
+ case 'AUTHV2_MFA_ALREADY_ACTIVE':
30662
+ return this.t('mfaErrorAlreadyActive');
30663
+ case 'AUTHV2_MFA_NOT_ENABLED':
30664
+ return this.t('mfaErrorNotEnabled');
30665
+ case 'AUTHV2_PHONE_REQUIRED':
30666
+ return this.t('mfaErrorPhoneRequired');
30667
+ case 'AUTHV2_PHONE_EXISTS':
30668
+ return this.t('mfaErrorPhoneExists');
30669
+ case 'AUTHV2_TOO_MANY_ATTEMPTS':
30670
+ return this.t('errorTooManyAttempts');
30671
+ case 'AUTHV2_INVALID_CURRENT_PASSWORD':
30672
+ return this.t('errorCurrentPasswordWrong');
30673
+ default:
30674
+ return this.t('errorGeneric');
30675
+ }
30676
+ }
30677
+ showToast(message) {
30678
+ this.toast.show({ message, duration: 3500 });
30679
+ }
30680
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
30681
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MfaModalComponent, isStandalone: true, selector: "val-mfa-modal", inputs: { isOpen: "isOpen", prefillCode: "prefillCode" }, outputs: { changed: "changed", dismissed: "dismissed" }, ngImport: i0, template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" (click)=\"close()\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') {\n <h2 class=\"mfa-title\">{{ t('mfaManageTitle') }}</h2>\n\n @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <h2 class=\"mfa-title\">{{ t('mfaEnableTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-title{font-size:18px;font-weight:700;margin:0;color:var(--ion-color-dark)}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: QrCodeComponent, selector: "val-qr-code", inputs: ["props"], outputs: ["actionComplete", "imageLoad", "imageError"] }, { kind: "component", type: PinInputComponent, selector: "val-pin-input", inputs: ["props"] }] }); }
30682
+ }
30683
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
30684
+ type: Component,
30685
+ args: [{ selector: 'val-mfa-modal', standalone: true, imports: [
30686
+ ReactiveFormsModule,
30687
+ IonButton,
30688
+ IonButtons,
30689
+ IonContent,
30690
+ IonHeader,
30691
+ IonIcon,
30692
+ IonInput,
30693
+ IonItem,
30694
+ IonLabel,
30695
+ IonModal,
30696
+ IonRadio,
30697
+ IonRadioGroup,
30698
+ IonSpinner,
30699
+ IonToolbar,
30700
+ FormComponent,
30701
+ QrCodeComponent,
30702
+ PinInputComponent,
30703
+ ], template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" (click)=\"close()\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') {\n <h2 class=\"mfa-title\">{{ t('mfaManageTitle') }}</h2>\n\n @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <h2 class=\"mfa-title\">{{ t('mfaEnableTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-title{font-size:18px;font-weight:700;margin:0;color:var(--ion-color-dark)}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"] }]
30704
+ }], ctorParameters: () => [], propDecorators: { isOpen: [{
30705
+ type: Input
30706
+ }], prefillCode: [{
30707
+ type: Input
30708
+ }], changed: [{
30709
+ type: Output
30710
+ }], dismissed: [{
30711
+ type: Output
30712
+ }] } });
30713
+
30129
30714
  /**
30130
30715
  * ItemListComponent
30131
30716
  *
@@ -46562,5 +47147,5 @@ function buildFooterLinks(links, t, resolver) {
46562
47147
  * Generated bundle index. Do not edit.
46563
47148
  */
46564
47149
 
46565
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, AppVersionService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, ModalService, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_SOCIAL_LINKS, VERSION, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechDebugConsole, provideValtechDonations, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
47150
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, AppVersionService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, MfaModalComponent, ModalService, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_SOCIAL_LINKS, VERSION, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechDebugConsole, provideValtechDonations, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
46566
47151
  //# sourceMappingURL=valtech-components.mjs.map