valtech-components 2.0.833 → 2.0.835

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.833';
56
+ const VERSION = '2.0.835';
57
57
 
58
58
  /**
59
59
  * Servicio para gestionar presets de componentes.
@@ -4342,6 +4342,70 @@ const VALTECH_DEFAULT_CONTENT = {
4342
4342
  passwordChangedSuccess: '¡Contraseña actualizada!',
4343
4343
  errorCurrentPasswordWrong: 'La contraseña actual es incorrecta',
4344
4344
  errorSamePassword: 'La nueva contraseña debe ser diferente a la actual',
4345
+ // Set password (cuenta OAuth-only)
4346
+ setPasswordTitle: 'Crear contraseña',
4347
+ setPasswordDescription: 'Tu cuenta usa inicio de sesión social. Crea una contraseña para entrar también con tu correo.',
4348
+ setPasswordSubmit: 'Crear contraseña',
4349
+ setPasswordConfirmTitle: '¿Crear contraseña?',
4350
+ setPasswordConfirmMessage: 'Podrás iniciar sesión con tu correo y contraseña, además de tu cuenta social. No perderás el acceso social.',
4351
+ setPasswordConfirmOk: 'Crear contraseña',
4352
+ setPasswordConfirmCancel: 'Cancelar',
4353
+ passwordSetSuccess: '¡Contraseña creada!',
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.',
4345
4409
  // Legal
4346
4410
  legalPrefix: 'Utilizamos los servicios de',
4347
4411
  legalSuffix: 'para ofrecerte una experiencia segura. Al iniciar sesión, aceptas nuestros',
@@ -4432,6 +4496,70 @@ const VALTECH_DEFAULT_CONTENT = {
4432
4496
  passwordChangedSuccess: 'Password updated!',
4433
4497
  errorCurrentPasswordWrong: 'Current password is incorrect',
4434
4498
  errorSamePassword: 'New password must be different from the current one',
4499
+ // Set password (OAuth-only account)
4500
+ setPasswordTitle: 'Create password',
4501
+ setPasswordDescription: 'Your account uses social sign-in. Create a password to also sign in with your email.',
4502
+ setPasswordSubmit: 'Create password',
4503
+ setPasswordConfirmTitle: 'Create password?',
4504
+ setPasswordConfirmMessage: 'You will be able to sign in with your email and password, in addition to your social account. You will not lose social access.',
4505
+ setPasswordConfirmOk: 'Create password',
4506
+ setPasswordConfirmCancel: 'Cancel',
4507
+ passwordSetSuccess: 'Password created!',
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.',
4435
4563
  // Legal
4436
4564
  legalPrefix: 'We use the services of',
4437
4565
  legalSuffix: 'to offer you a secure experience. By signing in, you accept our',
@@ -20518,7 +20646,7 @@ function isPublicEndpoint(request, authPrefix) {
20518
20646
  '/logout',
20519
20647
  '/mfa/verify', // Solo durante login
20520
20648
  ];
20521
- return publicEndpoints.some((endpoint) => request.url.includes(`${authPrefix}${endpoint}`));
20649
+ return publicEndpoints.some(endpoint => request.url.includes(`${authPrefix}${endpoint}`));
20522
20650
  }
20523
20651
  /**
20524
20652
  * Verifica si la request es a un endpoint que NO debe reintentar refresh en 401.
@@ -20527,6 +20655,9 @@ function isNoRefreshEndpoint(request, authPrefix) {
20527
20655
  // Endpoints que no deben intentar refresh en 401:
20528
20656
  // - Públicos: signin, signup, refresh, logout
20529
20657
  // - MFA: verify (durante login), confirm (401 = código incorrecto), disable (401 = contraseña incorrecta)
20658
+ // - change-password: 401 = contraseña actual incorrecta, NO token expirado.
20659
+ // Sin esta exclusión, el retry tras refresh vuelve a dar 401 y el
20660
+ // catchError de handle401Error dispara logout.
20530
20661
  const noRefreshEndpoints = [
20531
20662
  '/signin',
20532
20663
  '/signup',
@@ -20535,8 +20666,9 @@ function isNoRefreshEndpoint(request, authPrefix) {
20535
20666
  '/mfa/verify',
20536
20667
  '/mfa/confirm',
20537
20668
  '/mfa/disable',
20669
+ '/change-password',
20538
20670
  ];
20539
- return noRefreshEndpoints.some((endpoint) => request.url.includes(`${authPrefix}${endpoint}`));
20671
+ return noRefreshEndpoints.some(endpoint => request.url.includes(`${authPrefix}${endpoint}`));
20540
20672
  }
20541
20673
  /**
20542
20674
  * Maneja errores 401 refrescando el token.
@@ -20545,10 +20677,10 @@ function handle401Error(request, next, authService) {
20545
20677
  if (!isRefreshing) {
20546
20678
  isRefreshing = true;
20547
20679
  refreshTokenSubject.next(null);
20548
- return authService.refreshAccessToken().pipe(switchMap((response) => {
20680
+ return authService.refreshAccessToken().pipe(switchMap(response => {
20549
20681
  refreshTokenSubject.next(response.accessToken);
20550
20682
  return next(addAuthHeader(request, response.accessToken));
20551
- }), catchError((error) => {
20683
+ }), catchError(error => {
20552
20684
  authService.logout();
20553
20685
  return throwError(() => error);
20554
20686
  }), finalize(() => {
@@ -20556,7 +20688,7 @@ function handle401Error(request, next, authService) {
20556
20688
  }));
20557
20689
  }
20558
20690
  // Esperar a que termine el refresco en curso
20559
- return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => next(addAuthHeader(request, token))));
20691
+ return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap(token => next(addAuthHeader(request, token))));
20560
20692
  }
20561
20693
 
20562
20694
  /**
@@ -28924,16 +29056,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
28924
29056
  }], ctorParameters: () => [{ type: i2.ToastController }] });
28925
29057
 
28926
29058
  /**
28927
- * `val-change-password-modal` — modal para que un usuario autenticado cambie
28928
- * su contraseña. Análogo al modal de "recuperar contraseña" del `val-login`,
28929
- * pero para una sesión activa (vista de seguridad / settings).
29059
+ * `val-change-password-modal` — modal de gestión de contraseña para un usuario
29060
+ * autenticado. Análogo al modal de "recuperar contraseña" del `val-login`.
28930
29061
  *
28931
- * Self-contained: inyecta `AuthService` y llama `changePassword()` directo
28932
- * la app solo controla la visibilidad y reacciona a los eventos.
29062
+ * Es dual-mode: al abrirse consulta `AuthService.checkHasPassword()` y se adapta:
29063
+ * - **change** el user ya tiene contraseña pide actual + nueva →
29064
+ * `changePassword()`.
29065
+ * - **set** — el user es OAuth-only (sin contraseña) → pide solo la nueva,
29066
+ * pide confirmación explícita (no pierde el acceso social, es aditivo) →
29067
+ * `setPasswordForOAuthUser()`.
28933
29068
  *
28934
- * El gate de apertura lo controla el padre vía `[isOpen]`. El componente emite
28935
- * `changed` al éxito y `dismissed` cuando el user cierra el modal — el padre
28936
- * pone `isOpen` en `false` en ambos casos.
29069
+ * Self-contained: inyecta `AuthService` y llama el endpoint directo la app
29070
+ * solo controla `[isOpen]` y reacciona a `(changed)` / `(dismissed)`.
28937
29071
  *
28938
29072
  * i18n: usa el namespace compartido `_auth` (mismas claves que `val-login`).
28939
29073
  *
@@ -28947,10 +29081,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
28947
29081
  * ```
28948
29082
  */
28949
29083
  class ChangePasswordModalComponent {
29084
+ /**
29085
+ * Controla la visibilidad del modal. Lo decide el componente padre. Cada vez
29086
+ * que pasa de cerrado a abierto se resuelve el modo (change vs set).
29087
+ */
29088
+ set isOpen(value) {
29089
+ const opening = value && !this._isOpen;
29090
+ this._isOpen = value;
29091
+ if (opening) {
29092
+ this.resolveMode();
29093
+ }
29094
+ }
29095
+ get isOpen() {
29096
+ return this._isOpen;
29097
+ }
28950
29098
  constructor() {
28951
- /** Controla la visibilidad del modal. Lo decide el componente padre. */
28952
- this.isOpen = false;
28953
- /** Emite al cambiar la contraseña con éxito. El padre debe cerrar el modal. */
29099
+ this._isOpen = false;
29100
+ /** Emite al cambiar/crear la contraseña con éxito. El padre cierra el modal. */
28954
29101
  this.changed = new EventEmitter();
28955
29102
  /** Emite cuando el user cierra el modal (botón X o backdrop). */
28956
29103
  this.dismissed = new EventEmitter();
@@ -28958,65 +29105,112 @@ class ChangePasswordModalComponent {
28958
29105
  this.toast = inject(ToastService);
28959
29106
  this.i18n = inject(I18nService);
28960
29107
  this.i18nHelper = inject(InputI18nHelper);
29108
+ this.confirmDialog = inject(ConfirmationDialogService);
29109
+ this._mode = signal('loading');
29110
+ /** Modo actual — `loading` mientras se consulta `checkHasPassword()`. */
29111
+ this.mode = this._mode.asReadonly();
28961
29112
  this._formState = signal(ComponentStates.ENABLED);
28962
- this.formProps = computed(() => this.i18nHelper.resolveForm({
28963
- nameKey: 'changePasswordTitle',
28964
- i18nNamespace: '_auth',
28965
- sections: [
28966
- {
28967
- name: this.t('changePasswordDescription'),
28968
- order: 0,
28969
- fields: [
29113
+ this.formProps = computed(() => {
29114
+ if (this._mode() === 'set') {
29115
+ return this.i18nHelper.resolveForm({
29116
+ nameKey: 'setPasswordTitle',
29117
+ i18nNamespace: '_auth',
29118
+ sections: [
28970
29119
  {
28971
- type: InputType.PASSWORD,
28972
- name: 'currentPassword',
28973
- token: 'change-current-password',
28974
- labelKey: 'currentPassword',
28975
- hint: '',
28976
- placeholderKey: 'passwordPlaceholder',
28977
- errorKeys: {
28978
- required: 'currentPasswordRequired',
28979
- },
28980
- validators: [Validators.required],
29120
+ name: this.t('setPasswordDescription'),
28981
29121
  order: 0,
28982
- state: ComponentStates.ENABLED,
28983
- },
28984
- {
28985
- type: InputType.PASSWORD,
28986
- name: 'newPassword',
28987
- token: 'change-new-password',
28988
- labelKey: 'newPassword',
28989
- hintKey: 'newPasswordHint',
28990
- placeholderKey: 'passwordPlaceholder',
28991
- errorKeys: {
28992
- required: 'passwordRequired',
28993
- minlength: 'passwordMinLength',
28994
- },
28995
- validators: [Validators.required, Validators.minLength(8)],
28996
- order: 1,
28997
- state: ComponentStates.ENABLED,
29122
+ fields: [this.newPasswordField(0)],
28998
29123
  },
28999
29124
  ],
29125
+ actions: {
29126
+ ...SolidDefaultBlock('', 'submit'),
29127
+ token: 'set-submit',
29128
+ textKey: 'setPasswordSubmit',
29129
+ },
29130
+ state: this._formState(),
29131
+ });
29132
+ }
29133
+ return this.i18nHelper.resolveForm({
29134
+ nameKey: 'changePasswordTitle',
29135
+ i18nNamespace: '_auth',
29136
+ sections: [
29137
+ {
29138
+ name: this.t('changePasswordDescription'),
29139
+ order: 0,
29140
+ fields: [
29141
+ {
29142
+ type: InputType.PASSWORD,
29143
+ name: 'currentPassword',
29144
+ token: 'change-current-password',
29145
+ labelKey: 'currentPassword',
29146
+ hint: '',
29147
+ placeholderKey: 'passwordPlaceholder',
29148
+ errorKeys: {
29149
+ required: 'currentPasswordRequired',
29150
+ },
29151
+ validators: [Validators.required],
29152
+ order: 0,
29153
+ state: ComponentStates.ENABLED,
29154
+ },
29155
+ this.newPasswordField(1),
29156
+ ],
29157
+ },
29158
+ ],
29159
+ actions: {
29160
+ ...SolidDefaultBlock('', 'submit'),
29161
+ token: 'change-submit',
29162
+ textKey: 'changePasswordSubmit',
29000
29163
  },
29001
- ],
29002
- actions: {
29003
- ...SolidDefaultBlock('', 'submit'),
29004
- token: 'change-submit',
29005
- textKey: 'changePasswordSubmit',
29006
- },
29007
- state: this._formState(),
29008
- }));
29164
+ state: this._formState(),
29165
+ });
29166
+ });
29009
29167
  addIcons({ closeOutline });
29010
29168
  }
29011
29169
  /** Traduce una clave del namespace `_auth`. */
29012
29170
  t(key) {
29013
29171
  return this.i18n.t(key, '_auth');
29014
29172
  }
29173
+ /** Campo "nueva contraseña" — compartido por ambos modos. */
29174
+ newPasswordField(order) {
29175
+ return {
29176
+ type: InputType.PASSWORD,
29177
+ name: 'newPassword',
29178
+ token: 'change-new-password',
29179
+ labelKey: 'newPassword',
29180
+ hintKey: 'newPasswordHint',
29181
+ placeholderKey: 'passwordPlaceholder',
29182
+ errorKeys: {
29183
+ required: 'passwordRequired',
29184
+ minlength: 'passwordMinLength',
29185
+ },
29186
+ validators: [Validators.required, Validators.minLength(8)],
29187
+ order,
29188
+ state: ComponentStates.ENABLED,
29189
+ };
29190
+ }
29015
29191
  /** Cierre iniciado por el user (X / backdrop). */
29016
29192
  close() {
29017
29193
  this.dismissed.emit();
29018
29194
  }
29019
- submitHandler(event) {
29195
+ /** Consulta si el user ya tiene contraseña para elegir el modo del modal. */
29196
+ resolveMode() {
29197
+ this._mode.set('loading');
29198
+ this._formState.set(ComponentStates.ENABLED);
29199
+ this.auth.checkHasPassword().subscribe({
29200
+ next: res => this._mode.set(res.hasPassword ? 'change' : 'set'),
29201
+ // Fallback conservador: ante un fallo, asumir flujo de cambio normal.
29202
+ error: () => this._mode.set('change'),
29203
+ });
29204
+ }
29205
+ async submitHandler(event) {
29206
+ if (this._mode() === 'set') {
29207
+ await this.handleSetPassword(event);
29208
+ return;
29209
+ }
29210
+ this.handleChangePassword(event);
29211
+ }
29212
+ /** Flujo normal: el user tiene contraseña y la cambia. */
29213
+ handleChangePassword(event) {
29020
29214
  const currentPassword = event.fields['currentPassword'];
29021
29215
  const newPassword = event.fields['newPassword'];
29022
29216
  if (!currentPassword || !newPassword) {
@@ -29036,6 +29230,39 @@ class ChangePasswordModalComponent {
29036
29230
  },
29037
29231
  });
29038
29232
  }
29233
+ /**
29234
+ * Flujo OAuth-only: el user no tiene contraseña. Antes de crearla pide
29235
+ * confirmación explícita — es un cambio de cuenta, aunque aditivo (conserva
29236
+ * el acceso social).
29237
+ */
29238
+ async handleSetPassword(event) {
29239
+ const newPassword = event.fields['newPassword'];
29240
+ if (!newPassword) {
29241
+ this.showToast(this.t('completeAllFields'));
29242
+ return;
29243
+ }
29244
+ const result = await this.confirmDialog.confirm({
29245
+ title: this.t('setPasswordConfirmTitle'),
29246
+ message: this.t('setPasswordConfirmMessage'),
29247
+ confirmButton: { text: this.t('setPasswordConfirmOk'), role: 'confirm' },
29248
+ cancelButton: { text: this.t('setPasswordConfirmCancel'), role: 'cancel' },
29249
+ });
29250
+ if (!result.confirmed) {
29251
+ return;
29252
+ }
29253
+ this._formState.set(ComponentStates.WORKING);
29254
+ this.auth.setPasswordForOAuthUser(newPassword).subscribe({
29255
+ next: () => {
29256
+ this._formState.set(ComponentStates.ENABLED);
29257
+ this.showToast(this.t('passwordSetSuccess'));
29258
+ this.changed.emit();
29259
+ },
29260
+ error: err => {
29261
+ this._formState.set(ComponentStates.ENABLED);
29262
+ this.showToast(this.resolveError(err));
29263
+ },
29264
+ });
29265
+ }
29039
29266
  /** Mapea los códigos de error del backend a mensajes del namespace `_auth`. */
29040
29267
  resolveError(err) {
29041
29268
  const code = err?.code;
@@ -29044,6 +29271,8 @@ class ChangePasswordModalComponent {
29044
29271
  return this.t('errorCurrentPasswordWrong');
29045
29272
  case 'AUTHV2_SAME_PASSWORD':
29046
29273
  return this.t('errorSamePassword');
29274
+ case 'AUTHV2_PASSWORD_ALREADY_SET':
29275
+ return this.t('errorPasswordAlreadySet');
29047
29276
  default:
29048
29277
  return this.t('errorGeneric');
29049
29278
  }
@@ -29052,11 +29281,11 @@ class ChangePasswordModalComponent {
29052
29281
  this.toast.show({ message, duration: 3500 });
29053
29282
  }
29054
29283
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
29055
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: ChangePasswordModalComponent, isStandalone: true, selector: "val-change-password-modal", inputs: { isOpen: "isOpen" }, 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 <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}\n"], dependencies: [{ 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: IonModal, selector: "ion-modal" }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
29284
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChangePasswordModalComponent, isStandalone: true, selector: "val-change-password-modal", inputs: { isOpen: "isOpen" }, 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 <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"], dependencies: [{ 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: IonModal, selector: "ion-modal" }, { 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"] }] }); }
29056
29285
  }
29057
29286
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, decorators: [{
29058
29287
  type: Component,
29059
- args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonModal, IonToolbar, FormComponent], 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 <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}\n"] }]
29288
+ args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonModal, IonSpinner, IonToolbar, FormComponent], 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 <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"] }]
29060
29289
  }], ctorParameters: () => [], propDecorators: { isOpen: [{
29061
29290
  type: Input
29062
29291
  }], changed: [{
@@ -30005,6 +30234,408 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
30005
30234
  type: Output
30006
30235
  }] } });
30007
30236
 
30237
+ /** Segundos de espera antes de poder reenviar el código EMAIL/SMS. */
30238
+ const RESEND_COOLDOWN_SECONDS = 30;
30239
+ /**
30240
+ * `val-mfa-modal` — modal de gestión de autenticación de dos factores (MFA)
30241
+ * para un usuario autenticado. Mismo patrón que `val-change-password-modal`.
30242
+ *
30243
+ * Flujo (máquina de estados interna):
30244
+ * - `loading` → `getProfile()` para conocer el estado MFA.
30245
+ * - `status` → muestra MFA habilitado/deshabilitado. Si está habilitado:
30246
+ * gestión de backup codes (TOTP) + deshabilitar. Si no: botón habilitar.
30247
+ * - `method-select` → elegir TOTP / EMAIL / SMS.
30248
+ * - `totp-setup` → QR + secreto manual + backup codes → verificar código.
30249
+ * - `code-confirm` → (EMAIL/SMS) ingresar código recibido, con reenvío.
30250
+ * - `disable` → contraseña para deshabilitar MFA.
30251
+ *
30252
+ * El QR se genera **client-side** (`QrGeneratorService`) — el secreto TOTP
30253
+ * nunca sale del navegador.
30254
+ *
30255
+ * Self-contained: inyecta `AuthService` y llama los endpoints directo. La app
30256
+ * controla `[isOpen]` y reacciona a `(changed)` / `(dismissed)`.
30257
+ *
30258
+ * i18n: namespace compartido `_auth`.
30259
+ *
30260
+ * @example
30261
+ * ```html
30262
+ * <val-mfa-modal
30263
+ * [isOpen]="isModalOpen()"
30264
+ * (dismissed)="isModalOpen.set(false)"
30265
+ * />
30266
+ * ```
30267
+ */
30268
+ class MfaModalComponent {
30269
+ /** Controla la visibilidad. Cada apertura re-resuelve el estado MFA. */
30270
+ set isOpen(value) {
30271
+ const opening = value && !this._isOpen;
30272
+ this._isOpen = value;
30273
+ if (opening) {
30274
+ this.resolveStatus();
30275
+ }
30276
+ }
30277
+ get isOpen() {
30278
+ return this._isOpen;
30279
+ }
30280
+ constructor() {
30281
+ this._isOpen = false;
30282
+ /** Emite cuando el estado MFA cambia (habilitado / deshabilitado). */
30283
+ this.changed = new EventEmitter();
30284
+ /** Emite cuando el user cierra el modal (botón X o backdrop). */
30285
+ this.dismissed = new EventEmitter();
30286
+ this.auth = inject(AuthService);
30287
+ this.toast = inject(ToastService);
30288
+ this.i18n = inject(I18nService);
30289
+ this.qrGen = inject(QrGeneratorService);
30290
+ this._step = signal('loading');
30291
+ /** Paso actual del flujo. */
30292
+ this.step = this._step.asReadonly();
30293
+ /** `true` mientras una llamada al backend está en curso. */
30294
+ this.working = signal(false);
30295
+ // Estado MFA actual.
30296
+ this.mfaEnabled = signal(false);
30297
+ this.mfaMethod = signal(null);
30298
+ this.userPhone = signal(null);
30299
+ this.backupCodesCount = signal(0);
30300
+ // Estado del flujo de habilitación.
30301
+ this.selectedMethod = signal('TOTP');
30302
+ this.totpSetup = signal(null);
30303
+ this.totpQr = signal(null);
30304
+ /** Códigos de respaldo recién regenerados — se muestran una sola vez. */
30305
+ this.regeneratedCodes = signal(null);
30306
+ this.resendCooldown = signal(0);
30307
+ this.pinControl = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(6)]);
30308
+ this.passwordControl = new FormControl('', [Validators.required]);
30309
+ this.phoneControl = new FormControl('', [Validators.required, Validators.pattern(/^\+[1-9]\d{6,14}$/)]);
30310
+ this.pinInputProps = {
30311
+ control: this.pinControl,
30312
+ token: 'mfa-code',
30313
+ length: 6,
30314
+ allowNumbersOnly: true,
30315
+ autoFocus: true,
30316
+ };
30317
+ this.resendTimer = null;
30318
+ addIcons({ closeOutline });
30319
+ }
30320
+ ngOnDestroy() {
30321
+ this.stopCooldown();
30322
+ }
30323
+ /** Traduce una clave del namespace `_auth`. */
30324
+ t(key) {
30325
+ return this.i18n.t(key, '_auth');
30326
+ }
30327
+ /** Cierre iniciado por el user (X / backdrop). */
30328
+ close() {
30329
+ this.dismissed.emit();
30330
+ }
30331
+ // ===========================================================================
30332
+ // Carga de estado
30333
+ // ===========================================================================
30334
+ /** Consulta el perfil para conocer el estado MFA y posicionar el flujo. */
30335
+ resolveStatus() {
30336
+ this._step.set('loading');
30337
+ this.resetFlow();
30338
+ this.auth.getProfile().subscribe({
30339
+ next: profile => {
30340
+ this.mfaEnabled.set(profile.mfaEnabled);
30341
+ this.mfaMethod.set(profile.mfaMethod ?? null);
30342
+ this.userPhone.set(profile.phone ?? null);
30343
+ if (profile.mfaEnabled && profile.mfaMethod === 'TOTP') {
30344
+ this.loadBackupCount();
30345
+ }
30346
+ this._step.set('status');
30347
+ },
30348
+ error: () => {
30349
+ // Fallback: usar el signal de usuario en sesión.
30350
+ const user = this.auth.user();
30351
+ this.mfaEnabled.set(user?.mfaEnabled ?? false);
30352
+ this.mfaMethod.set(user?.mfaMethod ?? null);
30353
+ this._step.set('status');
30354
+ },
30355
+ });
30356
+ }
30357
+ loadBackupCount() {
30358
+ this.auth.getBackupCodesCount().subscribe({
30359
+ next: res => this.backupCodesCount.set(res.count),
30360
+ error: () => this.backupCodesCount.set(0),
30361
+ });
30362
+ }
30363
+ // ===========================================================================
30364
+ // Navegación entre pasos
30365
+ // ===========================================================================
30366
+ goToMethodSelect() {
30367
+ this.regeneratedCodes.set(null);
30368
+ this._step.set('method-select');
30369
+ }
30370
+ goToDisable() {
30371
+ this.passwordControl.reset();
30372
+ this._step.set('disable');
30373
+ }
30374
+ backToStatus() {
30375
+ this.stopCooldown();
30376
+ this.resolveStatus();
30377
+ }
30378
+ // ===========================================================================
30379
+ // Habilitar MFA
30380
+ // ===========================================================================
30381
+ /** Continúa desde el selector de método al setup correspondiente. */
30382
+ proceedWithMethod() {
30383
+ const method = this.selectedMethod();
30384
+ if (method === 'TOTP') {
30385
+ this.setupTotp();
30386
+ return;
30387
+ }
30388
+ let phone;
30389
+ if (method === 'SMS' && !this.userPhone()) {
30390
+ if (this.phoneControl.invalid) {
30391
+ this.phoneControl.markAsTouched();
30392
+ this.showToast(this.t('mfaPhoneInvalid'));
30393
+ return;
30394
+ }
30395
+ phone = this.phoneControl.value ?? undefined;
30396
+ }
30397
+ this.working.set(true);
30398
+ this.auth.setupMFA(method, phone).subscribe({
30399
+ next: res => {
30400
+ this.working.set(false);
30401
+ if (res.codeSent) {
30402
+ this.pinControl.reset();
30403
+ this._step.set('code-confirm');
30404
+ this.startCooldown();
30405
+ }
30406
+ },
30407
+ error: err => {
30408
+ this.working.set(false);
30409
+ this.showToast(this.resolveError(err));
30410
+ },
30411
+ });
30412
+ }
30413
+ setupTotp() {
30414
+ this.working.set(true);
30415
+ this.auth.setupTOTP().subscribe({
30416
+ next: res => {
30417
+ this.totpSetup.set(res);
30418
+ this.pinControl.reset();
30419
+ this.qrGen
30420
+ .generate({ data: res.qrCodeUrl, width: 220 })
30421
+ .then(qr => this.totpQr.set(qr))
30422
+ .catch(() => this.totpQr.set(null));
30423
+ this.working.set(false);
30424
+ this._step.set('totp-setup');
30425
+ },
30426
+ error: err => {
30427
+ this.working.set(false);
30428
+ this.showToast(this.resolveError(err));
30429
+ },
30430
+ });
30431
+ }
30432
+ /** Verifica el código TOTP de la app de autenticación y activa MFA. */
30433
+ verifyTotp() {
30434
+ const code = this.pinControl.value;
30435
+ if (!code || code.length !== 6) {
30436
+ this.showToast(this.t('mfaCodeInvalid'));
30437
+ return;
30438
+ }
30439
+ this.working.set(true);
30440
+ this.auth.verifyTOTPSetup(code).subscribe({
30441
+ next: res => {
30442
+ this.working.set(false);
30443
+ if (res.enabled) {
30444
+ this.showToast(this.t('mfaEnabledOk'));
30445
+ this.changed.emit();
30446
+ this.resolveStatus();
30447
+ }
30448
+ },
30449
+ error: err => {
30450
+ this.working.set(false);
30451
+ this.showToast(this.resolveError(err));
30452
+ },
30453
+ });
30454
+ }
30455
+ /** Confirma el código EMAIL/SMS y activa MFA. */
30456
+ confirmCode() {
30457
+ const code = this.pinControl.value;
30458
+ if (!code || code.length !== 6) {
30459
+ this.showToast(this.t('mfaCodeInvalid'));
30460
+ return;
30461
+ }
30462
+ this.working.set(true);
30463
+ this.auth.confirmMFA(code).subscribe({
30464
+ next: res => {
30465
+ this.working.set(false);
30466
+ if (res.mfaEnabled) {
30467
+ this.showToast(this.t('mfaEnabledOk'));
30468
+ this.changed.emit();
30469
+ this.resolveStatus();
30470
+ }
30471
+ },
30472
+ error: err => {
30473
+ this.working.set(false);
30474
+ this.showToast(this.resolveError(err));
30475
+ },
30476
+ });
30477
+ }
30478
+ /** Reenvía el código EMAIL/SMS (re-ejecuta el setup). */
30479
+ resendCode() {
30480
+ if (this.resendCooldown() > 0) {
30481
+ return;
30482
+ }
30483
+ this.proceedWithMethod();
30484
+ }
30485
+ // ===========================================================================
30486
+ // Gestión (MFA habilitado)
30487
+ // ===========================================================================
30488
+ /** Regenera los códigos de respaldo TOTP y los muestra una vez. */
30489
+ regenerateBackupCodes() {
30490
+ this.working.set(true);
30491
+ this.auth.regenerateBackupCodes().subscribe({
30492
+ next: res => {
30493
+ this.working.set(false);
30494
+ this.backupCodesCount.set(res.backupCodes.length);
30495
+ this.regeneratedCodes.set(res.backupCodes);
30496
+ },
30497
+ error: err => {
30498
+ this.working.set(false);
30499
+ this.showToast(this.resolveError(err));
30500
+ },
30501
+ });
30502
+ }
30503
+ /** Deshabilita MFA — requiere la contraseña de la cuenta. */
30504
+ confirmDisable() {
30505
+ const password = this.passwordControl.value;
30506
+ if (!password) {
30507
+ this.showToast(this.t('mfaPasswordRequired'));
30508
+ return;
30509
+ }
30510
+ this.working.set(true);
30511
+ this.auth.disableMFA(password).subscribe({
30512
+ next: res => {
30513
+ this.working.set(false);
30514
+ if (res.mfaDisabled) {
30515
+ this.showToast(this.t('mfaDisabledOk'));
30516
+ this.changed.emit();
30517
+ this.resolveStatus();
30518
+ }
30519
+ },
30520
+ error: err => {
30521
+ this.working.set(false);
30522
+ this.showToast(this.resolveError(err));
30523
+ },
30524
+ });
30525
+ }
30526
+ /** Copia una lista de códigos de respaldo al portapapeles. */
30527
+ async copyCodes(codes) {
30528
+ try {
30529
+ await navigator.clipboard.writeText(codes.join('\n'));
30530
+ this.showToast(this.t('mfaCodesCopied'));
30531
+ }
30532
+ catch {
30533
+ /* sin recurso de copia disponible */
30534
+ }
30535
+ }
30536
+ /** Etiqueta i18n legible para un método MFA. */
30537
+ methodLabel(method) {
30538
+ switch (method) {
30539
+ case 'TOTP':
30540
+ return this.t('mfaMethodTotp');
30541
+ case 'EMAIL':
30542
+ return this.t('mfaMethodEmail');
30543
+ case 'SMS':
30544
+ return this.t('mfaMethodSms');
30545
+ default:
30546
+ return '';
30547
+ }
30548
+ }
30549
+ // ===========================================================================
30550
+ // Helpers
30551
+ // ===========================================================================
30552
+ resetFlow() {
30553
+ this.pinControl.reset();
30554
+ this.passwordControl.reset();
30555
+ this.phoneControl.reset();
30556
+ this.totpSetup.set(null);
30557
+ this.totpQr.set(null);
30558
+ this.regeneratedCodes.set(null);
30559
+ this.selectedMethod.set('TOTP');
30560
+ this.stopCooldown();
30561
+ }
30562
+ startCooldown() {
30563
+ this.stopCooldown();
30564
+ this.resendCooldown.set(RESEND_COOLDOWN_SECONDS);
30565
+ this.resendTimer = setInterval(() => {
30566
+ this.resendCooldown.update(v => v - 1);
30567
+ if (this.resendCooldown() <= 0) {
30568
+ this.stopCooldown();
30569
+ }
30570
+ }, 1000);
30571
+ }
30572
+ stopCooldown() {
30573
+ if (this.resendTimer) {
30574
+ clearInterval(this.resendTimer);
30575
+ this.resendTimer = null;
30576
+ }
30577
+ this.resendCooldown.set(0);
30578
+ }
30579
+ /** Mapea los códigos de error MFA del backend a mensajes del namespace `_auth`. */
30580
+ resolveError(err) {
30581
+ const code = err?.code;
30582
+ switch (code) {
30583
+ case 'AUTHV2_MFA_INVALID_CODE':
30584
+ return this.t('mfaErrorInvalidCode');
30585
+ case 'AUTHV2_EXPIRED_CODE':
30586
+ return this.t('mfaErrorExpiredCode');
30587
+ case 'AUTHV2_CODE_USED':
30588
+ return this.t('mfaErrorCodeUsed');
30589
+ case 'AUTHV2_MFA_ALREADY_ACTIVE':
30590
+ return this.t('mfaErrorAlreadyActive');
30591
+ case 'AUTHV2_MFA_NOT_ENABLED':
30592
+ return this.t('mfaErrorNotEnabled');
30593
+ case 'AUTHV2_PHONE_REQUIRED':
30594
+ return this.t('mfaErrorPhoneRequired');
30595
+ case 'AUTHV2_PHONE_EXISTS':
30596
+ return this.t('mfaErrorPhoneExists');
30597
+ case 'AUTHV2_TOO_MANY_ATTEMPTS':
30598
+ return this.t('errorTooManyAttempts');
30599
+ case 'AUTHV2_INVALID_CURRENT_PASSWORD':
30600
+ return this.t('errorCurrentPasswordWrong');
30601
+ default:
30602
+ return this.t('errorGeneric');
30603
+ }
30604
+ }
30605
+ showToast(message) {
30606
+ this.toast.show({ message, duration: 3500 });
30607
+ }
30608
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
30609
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MfaModalComponent, isStandalone: true, selector: "val-mfa-modal", inputs: { isOpen: "isOpen" }, 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-muted\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n @if (backupCodesCount() < 3) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesLow') }}</p>\n } @if (regeneratedCodes(); as codes) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\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\" fill=\"outline\" (click)=\"copyCodes(codes)\"> {{ t('mfaCopyCodes') }} </ion-button>\n } @else {\n <ion-button expand=\"block\" fill=\"outline\" [disabled]=\"working()\" (click)=\"regenerateBackupCodes()\">\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=\"danger\" 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-muted\">{{ 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-muted\">{{ 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 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-muted\">{{ 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=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-muted\">{{ 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-muted\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-muted\">{{ 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-block mfa-block--warn\">\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\n <p class=\"mfa-muted\">{{ t('mfaBackupCodesExplain') }}</p>\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\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-muted\">\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-muted\">{{ 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=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <h2 class=\"mfa-title\">{{ t('mfaDisableTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaDisablePrompt') }}</p>\n\n <ion-input\n label=\"{{ t('mfaPasswordLabel') }}\"\n labelPlacement=\"floating\"\n type=\"password\"\n [formControl]=\"passwordControl\"\n ></ion-input>\n\n <ion-button\n expand=\"block\"\n color=\"danger\"\n [disabled]=\"working() || passwordControl.invalid\"\n (click)=\"confirmDisable()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaDisableButton') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{max-width:460px;margin:0 auto;display:flex;flex-direction:column;gap:12px;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-muted{color:var(--ion-color-medium);font-size:14px;margin:0}.mfa-warn{color:var(--ion-color-warning-shade);font-size:14px;font-weight:600;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block{display:flex;flex-direction:column;gap:10px;padding:14px;border-radius:12px;background:var(--ion-color-light)}.mfa-block--warn{background:var(--ion-color-warning-tint, rgba(255, 196, 9, .12));border:1px solid var(--ion-color-warning)}.mfa-block h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:12px 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}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);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-medium);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: QrCodeComponent, selector: "val-qr-code", inputs: ["props"], outputs: ["actionComplete", "imageLoad", "imageError"] }, { kind: "component", type: PinInputComponent, selector: "val-pin-input", inputs: ["props"] }] }); }
30610
+ }
30611
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
30612
+ type: Component,
30613
+ args: [{ selector: 'val-mfa-modal', standalone: true, imports: [
30614
+ ReactiveFormsModule,
30615
+ IonButton,
30616
+ IonButtons,
30617
+ IonContent,
30618
+ IonHeader,
30619
+ IonIcon,
30620
+ IonInput,
30621
+ IonItem,
30622
+ IonLabel,
30623
+ IonModal,
30624
+ IonRadio,
30625
+ IonRadioGroup,
30626
+ IonSpinner,
30627
+ IonToolbar,
30628
+ QrCodeComponent,
30629
+ PinInputComponent,
30630
+ ], 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-muted\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n @if (backupCodesCount() < 3) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesLow') }}</p>\n } @if (regeneratedCodes(); as codes) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\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\" fill=\"outline\" (click)=\"copyCodes(codes)\"> {{ t('mfaCopyCodes') }} </ion-button>\n } @else {\n <ion-button expand=\"block\" fill=\"outline\" [disabled]=\"working()\" (click)=\"regenerateBackupCodes()\">\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=\"danger\" 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-muted\">{{ 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-muted\">{{ 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 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-muted\">{{ 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=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-muted\">{{ 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-muted\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-muted\">{{ 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-block mfa-block--warn\">\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\n <p class=\"mfa-muted\">{{ t('mfaBackupCodesExplain') }}</p>\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\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-muted\">\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-muted\">{{ 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=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <h2 class=\"mfa-title\">{{ t('mfaDisableTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaDisablePrompt') }}</p>\n\n <ion-input\n label=\"{{ t('mfaPasswordLabel') }}\"\n labelPlacement=\"floating\"\n type=\"password\"\n [formControl]=\"passwordControl\"\n ></ion-input>\n\n <ion-button\n expand=\"block\"\n color=\"danger\"\n [disabled]=\"working() || passwordControl.invalid\"\n (click)=\"confirmDisable()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaDisableButton') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{max-width:460px;margin:0 auto;display:flex;flex-direction:column;gap:12px;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-muted{color:var(--ion-color-medium);font-size:14px;margin:0}.mfa-warn{color:var(--ion-color-warning-shade);font-size:14px;font-weight:600;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block{display:flex;flex-direction:column;gap:10px;padding:14px;border-radius:12px;background:var(--ion-color-light)}.mfa-block--warn{background:var(--ion-color-warning-tint, rgba(255, 196, 9, .12));border:1px solid var(--ion-color-warning)}.mfa-block h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:12px 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}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);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-medium);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"] }]
30631
+ }], ctorParameters: () => [], propDecorators: { isOpen: [{
30632
+ type: Input
30633
+ }], changed: [{
30634
+ type: Output
30635
+ }], dismissed: [{
30636
+ type: Output
30637
+ }] } });
30638
+
30008
30639
  /**
30009
30640
  * ItemListComponent
30010
30641
  *
@@ -46441,5 +47072,5 @@ function buildFooterLinks(links, t, resolver) {
46441
47072
  * Generated bundle index. Do not edit.
46442
47073
  */
46443
47074
 
46444
- 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 };
47075
+ 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 };
46445
47076
  //# sourceMappingURL=valtech-components.mjs.map