valtech-components 2.0.839 → 2.0.841

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.
@@ -5,7 +5,7 @@ import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal,
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgStyle, NgFor, isPlatformBrowser, DOCUMENT, NgClass } from '@angular/common';
7
7
  import { addIcons } from 'ionicons';
8
- import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, cameraOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline } from 'ionicons/icons';
8
+ import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, warningOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, cameraOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline } from 'ionicons/icons';
9
9
  import * as i1$1 from '@angular/router';
10
10
  import { RouterLink, Router, NavigationEnd, RouterOutlet, RouterModule } from '@angular/router';
11
11
  import { Browser } from '@capacitor/browser';
@@ -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.839';
56
+ const VERSION = '2.0.841';
57
57
 
58
58
  /**
59
59
  * Servicio para gestionar presets de componentes.
@@ -360,6 +360,8 @@ class IconService {
360
360
  keyOutline,
361
361
  desktopOutline,
362
362
  logOutOutline,
363
+ cloudDownloadOutline,
364
+ warningOutline,
363
365
  });
364
366
  }
365
367
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: IconService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -4412,6 +4414,8 @@ const VALTECH_DEFAULT_CONTENT = {
4412
4414
  termsAndConditions: 'Términos y Condiciones',
4413
4415
  and: 'y',
4414
4416
  privacyPolicy: 'Política de Privacidad',
4417
+ // Acciones genéricas
4418
+ close: 'Cerrar',
4415
4419
  // Toasts
4416
4420
  welcome: '¡Bienvenido!',
4417
4421
  completeAllFields: 'Completa todos los campos.',
@@ -4566,6 +4570,8 @@ const VALTECH_DEFAULT_CONTENT = {
4566
4570
  termsAndConditions: 'Terms and Conditions',
4567
4571
  and: 'and',
4568
4572
  privacyPolicy: 'Privacy Policy',
4573
+ // Acciones genéricas
4574
+ close: 'Close',
4569
4575
  // Toasts
4570
4576
  welcome: 'Welcome!',
4571
4577
  completeAllFields: 'Complete all fields.',
@@ -20781,6 +20787,454 @@ function provideValtechAuthInterceptor() {
20781
20787
  return makeEnvironmentProviders([provideHttpClient(withInterceptors([authInterceptor]))]);
20782
20788
  }
20783
20789
 
20790
+ /**
20791
+ * Error interpretation helper for the Valtech factory.
20792
+ *
20793
+ * Todos los frontends del factory consumen la misma API (backend Go) y la misma
20794
+ * librería. La lógica de interpretar errores debe vivir una sola vez, acá.
20795
+ *
20796
+ * El backend Go (`apperrors`) SIEMPRE devuelve errores como JSON:
20797
+ * { "code": string, "message": string, "operationId": string }
20798
+ * donde `message` ya viene en español y es user-friendly.
20799
+ *
20800
+ * En Angular un error HTTP llega como `HttpErrorResponse` (body en `.error`).
20801
+ * Un fallo de red es un `HttpErrorResponse` con `status === 0`.
20802
+ *
20803
+ * Además `AuthService.handleAuthError` aplana el `HttpErrorResponse` a un
20804
+ * `AuthError { code, message }` (code/message al nivel superior). Por eso este
20805
+ * helper acepta AMBAS formas — el crudo y el aplanado.
20806
+ */
20807
+ /** Mensaje genérico para fallos de red (sin conexión / backend inalcanzable). */
20808
+ const NETWORK_MESSAGE = 'Sin conexión. Verifica tu conexión a internet e inténtalo de nuevo.';
20809
+ /** Mensaje genérico para errores no identificables. */
20810
+ const UNKNOWN_MESSAGE = 'Ocurrió un error inesperado. Inténtalo de nuevo.';
20811
+ /** Sentinel para fallos de red. */
20812
+ const NETWORK_CODE = 'NETWORK';
20813
+ /** Sentinel para errores no identificables. */
20814
+ const UNKNOWN_CODE = 'UNKNOWN';
20815
+ /** Type guard laxo: ¿el valor parece un `HttpErrorResponse`? */
20816
+ function isHttpErrorResponse(err) {
20817
+ return (typeof err === 'object' &&
20818
+ err !== null &&
20819
+ // No importamos HttpErrorResponse para mantener la fn libre de Angular;
20820
+ // detectamos por shape: tiene `status` numérico y `name` reconocible o `error`.
20821
+ ('status' in err || err.name === 'HttpErrorResponse'));
20822
+ }
20823
+ /** Devuelve un string si el valor lo es y no está vacío; si no, `undefined`. */
20824
+ function asNonEmptyString(value) {
20825
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
20826
+ }
20827
+ /**
20828
+ * Normaliza CUALQUIER error a un `InterpretedError`.
20829
+ *
20830
+ * Función pura — sin dependencias de Angular DI, testeable y usable desde
20831
+ * cualquier lado (componentes, servicios, interceptores, scripts).
20832
+ *
20833
+ * Nunca lanza: siempre devuelve un `InterpretedError` válido.
20834
+ *
20835
+ * Casos cubiertos:
20836
+ * - `HttpErrorResponse` con `status === 0` (o sin respuesta) → fallo de red.
20837
+ * - `HttpErrorResponse` con body `{ code, message, operationId }` del backend.
20838
+ * - `AuthError` aplanado `{ code, message }` (code/message top-level).
20839
+ * - `Error` plano de JS → `code: 'UNKNOWN'`, `message: err.message`.
20840
+ * - Cualquier otra cosa (string, null, undefined, objeto raro) → genérico.
20841
+ *
20842
+ * @example
20843
+ * ```ts
20844
+ * try {
20845
+ * await firstValueFrom(this.http.get(url));
20846
+ * } catch (err) {
20847
+ * const e = interpretError(err);
20848
+ * if (e.isNetwork) {
20849
+ * this.toast.show({ message: e.message, color: 'dark' });
20850
+ * } else {
20851
+ * this.errorCode.set(e.code);
20852
+ * }
20853
+ * }
20854
+ * ```
20855
+ */
20856
+ function interpretError(err) {
20857
+ // 1. HttpErrorResponse — el caso más común al hablar con el backend.
20858
+ if (isHttpErrorResponse(err)) {
20859
+ const status = typeof err.status === 'number' ? err.status : undefined;
20860
+ // 1a. Fallo de red: status 0 o sin body de respuesta del servidor.
20861
+ if (status === 0) {
20862
+ return {
20863
+ code: NETWORK_CODE,
20864
+ message: NETWORK_MESSAGE,
20865
+ status: 0,
20866
+ isNetwork: true,
20867
+ };
20868
+ }
20869
+ // 1b. Body del backend `{ code, message, operationId }`.
20870
+ const body = err.error;
20871
+ if (typeof body === 'object' && body !== null) {
20872
+ const b = body;
20873
+ return {
20874
+ code: asNonEmptyString(b.code) ?? UNKNOWN_CODE,
20875
+ message: asNonEmptyString(b.message) ?? UNKNOWN_MESSAGE,
20876
+ operationId: asNonEmptyString(b.operationId),
20877
+ status,
20878
+ isNetwork: false,
20879
+ };
20880
+ }
20881
+ // 1c. HttpErrorResponse sin body estructurado (ej. body string / null).
20882
+ return {
20883
+ code: UNKNOWN_CODE,
20884
+ message: asNonEmptyString(err.message) ?? UNKNOWN_MESSAGE,
20885
+ status,
20886
+ isNetwork: false,
20887
+ };
20888
+ }
20889
+ // 2. AuthError aplanado u objeto con `code`/`message` top-level.
20890
+ if (typeof err === 'object' && err !== null) {
20891
+ const o = err;
20892
+ const code = asNonEmptyString(o.code);
20893
+ const message = asNonEmptyString(o.message);
20894
+ if (code || message) {
20895
+ return {
20896
+ code: code ?? UNKNOWN_CODE,
20897
+ message: message ?? UNKNOWN_MESSAGE,
20898
+ operationId: asNonEmptyString(o.operationId),
20899
+ isNetwork: false,
20900
+ };
20901
+ }
20902
+ // 2b. Error plano de JS (instancia de Error sin code) — `message` ya
20903
+ // cubierto arriba; este branch atrapa Error con message vacío.
20904
+ if (err instanceof Error) {
20905
+ return {
20906
+ code: UNKNOWN_CODE,
20907
+ message: asNonEmptyString(err.message) ?? UNKNOWN_MESSAGE,
20908
+ isNetwork: false,
20909
+ };
20910
+ }
20911
+ }
20912
+ // 3. Cualquier otra cosa: string, null, undefined, objeto raro.
20913
+ return {
20914
+ code: UNKNOWN_CODE,
20915
+ message: UNKNOWN_MESSAGE,
20916
+ isNetwork: false,
20917
+ };
20918
+ }
20919
+
20920
+ /**
20921
+ * Interceptor HTTP de observabilidad (Capa 1 del estándar de manejo de errores).
20922
+ *
20923
+ * Para CADA respuesta HTTP con error:
20924
+ * 1. Normaliza el error con `interpretError`.
20925
+ * 2. Loguea un evento estructurado a consola con el prefijo `[HTTP]`.
20926
+ * 3. Reporta el error a Firebase Analytics (`source: 'http'`), si hay analytics.
20927
+ *
20928
+ * **NO traga el error** — lo re-lanza tal cual. Las páginas y servicios siguen
20929
+ * recibiendo el error en su `catch` / `catchError` y deciden la UX (eso es
20930
+ * Capa 3: `ValtechErrorService`).
20931
+ *
20932
+ * `AnalyticsService` se inyecta `@Optional()` — apps sin Firebase Analytics
20933
+ * igual obtienen el log estructurado a consola.
20934
+ *
20935
+ * Decisión sobre 401: **se saltea** del tracking de Analytics. Un 401 es el
20936
+ * disparador normal del flujo de refresh de token del `authInterceptor`; el
20937
+ * token se renueva y la request se reintenta de forma transparente. Trackear
20938
+ * cada 401 inundaría Analytics de ruido (cada sesión genera varios al expirar
20939
+ * el access token). El 401 SÍ se loguea a consola (nivel `info`, no `error`)
20940
+ * para no perder la traza en debugging local, pero no se reporta como error.
20941
+ *
20942
+ * Se registra vía {@link provideValtechErrorHandling}.
20943
+ */
20944
+ const errorLoggingInterceptor = (request, next) => {
20945
+ // `AnalyticsService` puede no estar provisto (apps sin Firebase). El inject
20946
+ // funcional con `optional: true` devuelve `null` en ese caso.
20947
+ const analytics = inject(AnalyticsService, { optional: true });
20948
+ return next(request).pipe(catchError((error) => {
20949
+ const interpreted = interpretError(error);
20950
+ const status = interpreted.status ?? error?.status;
20951
+ // 401 → ruido del refresh de auth. Log informativo, sin Analytics.
20952
+ if (status === 401) {
20953
+ console.info(`[HTTP] 401 ${request.method} ${request.url}` +
20954
+ (interpreted.operationId ? ` (op=${interpreted.operationId})` : ''));
20955
+ return throwError(() => error);
20956
+ }
20957
+ // Log estructurado a consola — siempre, haya o no Analytics.
20958
+ console.error('[HTTP] request failed', {
20959
+ method: request.method,
20960
+ url: request.url,
20961
+ status: status ?? 'n/a',
20962
+ code: interpreted.code,
20963
+ operationId: interpreted.operationId ?? 'n/a',
20964
+ isNetwork: interpreted.isNetwork,
20965
+ message: interpreted.message,
20966
+ });
20967
+ // Reporte a Analytics (best-effort) — solo si hay servicio.
20968
+ if (analytics) {
20969
+ try {
20970
+ analytics.logError(interpreted.message, {
20971
+ source: 'http',
20972
+ code: interpreted.code,
20973
+ url: request.url,
20974
+ method: request.method,
20975
+ status: String(status ?? 0),
20976
+ ...(interpreted.operationId ? { operationId: interpreted.operationId } : {}),
20977
+ ...(interpreted.isNetwork ? { error_category: 'network' } : {}),
20978
+ });
20979
+ }
20980
+ catch (trackingError) {
20981
+ // El tracking nunca debe romper el flujo HTTP.
20982
+ console.warn('[HTTP] analytics tracking failed:', trackingError);
20983
+ }
20984
+ }
20985
+ // Re-lanzar: NO tragamos el error. La Capa 3 / las páginas lo manejan.
20986
+ return throwError(() => error);
20987
+ }));
20988
+ };
20989
+
20990
+ /**
20991
+ * Provee el manejo de errores estándar del factory Valtech.
20992
+ *
20993
+ * Registra el {@link errorLoggingInterceptor} (Capa 1 — observabilidad): toda
20994
+ * respuesta HTTP con error se normaliza, se loguea de forma estructurada a
20995
+ * consola y se reporta a Firebase Analytics. El interceptor **re-lanza** el
20996
+ * error, así que las páginas siguen haciendo su `catch`.
20997
+ *
20998
+ * Se registra vía `withInterceptors`, igual que el `authInterceptor`. Angular
20999
+ * fusiona los interceptores de múltiples llamadas a `provideHttpClient` dentro
21000
+ * del mismo injector, así que esto compone con `provideValtechAuth()` sin
21001
+ * pisarlo. El orden relativo lo determina Angular por orden de provisión;
21002
+ * para una traza limpia, declarar `provideValtechErrorHandling()` **después**
21003
+ * de `provideValtechAuth(...)` en `main.ts`.
21004
+ *
21005
+ * La Capa 3 ({@link ValtechErrorService}) es `providedIn: 'root'` — no
21006
+ * requiere registro. La Capa 2 (`AnalyticsErrorHandler`, errores no
21007
+ * capturados) la activa `provideValtechFirebase` con `enableErrorTracking`.
21008
+ *
21009
+ * @example
21010
+ * ```typescript
21011
+ * // main.ts
21012
+ * import { provideValtechAuth, provideValtechErrorHandling } from 'valtech-components';
21013
+ *
21014
+ * bootstrapApplication(AppComponent, {
21015
+ * providers: [
21016
+ * provideValtechAuth({ apiUrl: environment.apiUrl }),
21017
+ * provideValtechErrorHandling(),
21018
+ * ],
21019
+ * });
21020
+ * ```
21021
+ */
21022
+ function provideValtechErrorHandling() {
21023
+ return makeEnvironmentProviders([provideHttpClient(withInterceptors([errorLoggingInterceptor]))]);
21024
+ }
21025
+
21026
+ /**
21027
+ * Service for displaying toast notifications using Ionic's ToastController.
21028
+ * Provides methods to show and present toasts with custom options.
21029
+ */
21030
+ class ToastService {
21031
+ constructor(toastController) {
21032
+ this.toastController = toastController;
21033
+ }
21034
+ /**
21035
+ * Presents a toast notification with the given options.
21036
+ *
21037
+ * Estándar Valtech: todos los toasts son `color: 'dark'` y `position: 'top'`.
21038
+ * Estos son los defaults cuando el caller no los especifica — no hace falta
21039
+ * pasarlos en cada llamada. El diferenciador semántico (éxito/error) va en
21040
+ * el mensaje, no en el color.
21041
+ *
21042
+ * @param request Toast options (message, duration, position, color, etc.)
21043
+ */
21044
+ async presentToast(request) {
21045
+ const toast = await this.toastController.create({
21046
+ message: request.message,
21047
+ duration: request.duration,
21048
+ position: request.position ?? 'top',
21049
+ color: request.color ?? 'dark',
21050
+ });
21051
+ await toast.present();
21052
+ }
21053
+ /**
21054
+ * Shows a toast notification and logs the result.
21055
+ * @param request Toast options (message, duration, position, color, etc.)
21056
+ */
21057
+ show(request) {
21058
+ this.presentToast(request)
21059
+ .then(() => {
21060
+ console.info('Toast created');
21061
+ })
21062
+ .catch(error => {
21063
+ console.error(JSON.stringify(error));
21064
+ });
21065
+ }
21066
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, deps: [{ token: i2.ToastController }], target: i0.ɵɵFactoryTarget.Injectable }); }
21067
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, providedIn: 'root' }); }
21068
+ }
21069
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, decorators: [{
21070
+ type: Injectable,
21071
+ args: [{
21072
+ providedIn: 'root',
21073
+ }]
21074
+ }], ctorParameters: () => [{ type: i2.ToastController }] });
21075
+
21076
+ /**
21077
+ * Key i18n fija de la librería para fallos de red. Las apps pueden definirla en
21078
+ * su contenido i18n (namespace `_global`) para personalizar el texto; si no
21079
+ * existe, se usa {@link NETWORK_FALLBACK_MESSAGE}.
21080
+ */
21081
+ const VALTECH_NETWORK_ERROR_KEY = 'valtech.error.network';
21082
+ /** Texto en español por defecto cuando {@link VALTECH_NETWORK_ERROR_KEY} no está definida. */
21083
+ const NETWORK_FALLBACK_MESSAGE = 'Sin conexión. Revisá tu internet.';
21084
+ /** Texto genérico en español cuando no hay match ni `fallbackKey`. */
21085
+ const GENERIC_FALLBACK_MESSAGE = 'Ocurrió un error. Intentá de nuevo.';
21086
+ /**
21087
+ * Servicio de UX de errores capturados (Capa 3 del estándar de manejo de errores).
21088
+ *
21089
+ * Mientras la Capa 1 ({@link errorLoggingInterceptor}) observa TODAS las
21090
+ * respuestas HTTP con error, y la Capa 2 (`AnalyticsErrorHandler`) captura los
21091
+ * errores NO manejados, la Capa 3 es el punto único que una página usa en su
21092
+ * `catch` para convertir un error en feedback al usuario.
21093
+ *
21094
+ * Responsabilidades de {@link handle}:
21095
+ * 1. Normaliza el error (`interpretError`).
21096
+ * 2. Loguea a consola + reporta a Analytics (`source: 'handled'`, con `context`).
21097
+ * 3. Resuelve un mensaje localizado (ver orden de resolución en {@link handle}).
21098
+ * 4. Muestra un toast (`color: 'dark'`, estándar Valtech) salvo `toast: false`.
21099
+ * 5. Devuelve el {@link InterpretedError} para que la página decida lógica extra.
21100
+ *
21101
+ * **Nunca lanza.** `AnalyticsService` se inyecta `@Optional()`.
21102
+ *
21103
+ * @example
21104
+ * ```ts
21105
+ * private errors = inject(ValtechErrorService);
21106
+ *
21107
+ * async save() {
21108
+ * try {
21109
+ * await firstValueFrom(this.api.updateProfile(this.form.value));
21110
+ * this.toast.show({ message: 'Perfil actualizado', color: 'dark' });
21111
+ * } catch (err) {
21112
+ * this.errors.handle(err, {
21113
+ * context: 'profile.save',
21114
+ * i18nMap: { EMAIL_TAKEN: 'errors.emailTaken' },
21115
+ * fallbackKey: 'errors.profileSaveFailed',
21116
+ * });
21117
+ * }
21118
+ * }
21119
+ * ```
21120
+ */
21121
+ class ValtechErrorService {
21122
+ constructor() {
21123
+ this.i18n = inject(I18nService);
21124
+ this.toast = inject(ToastService);
21125
+ // Apps sin Firebase Analytics: el servicio sigue funcionando (log + toast).
21126
+ this.analytics = inject(AnalyticsService, { optional: true });
21127
+ }
21128
+ /**
21129
+ * Maneja un error capturado: normaliza, observa y produce feedback de usuario.
21130
+ *
21131
+ * Orden de resolución del mensaje del toast:
21132
+ * 1. `i18nMap[code]` definido → `i18n.t(esa key)`.
21133
+ * 2. Sin match y `isNetwork` → key de red ({@link VALTECH_NETWORK_ERROR_KEY}),
21134
+ * con fallback al texto español si la app no la tradujo.
21135
+ * 3. `fallbackKey` definido → `i18n.t(fallbackKey)`.
21136
+ * 4. Genérico en español.
21137
+ *
21138
+ * @param err Cualquier error capturado.
21139
+ * @param opts Opciones de contexto, mapeo i18n y toast.
21140
+ * @returns El {@link InterpretedError} normalizado. Nunca lanza.
21141
+ */
21142
+ handle(err, opts = {}) {
21143
+ const interpreted = interpretError(err);
21144
+ const { context, i18nMap, fallbackKey, i18nNamespace, toast } = opts;
21145
+ // 1. Observabilidad — log estructurado a consola.
21146
+ console.error('[ValtechError] handled', {
21147
+ context: context ?? 'n/a',
21148
+ code: interpreted.code,
21149
+ status: interpreted.status ?? 'n/a',
21150
+ operationId: interpreted.operationId ?? 'n/a',
21151
+ isNetwork: interpreted.isNetwork,
21152
+ message: interpreted.message,
21153
+ });
21154
+ // 2. Reporte a Analytics (best-effort, opcional).
21155
+ if (this.analytics) {
21156
+ try {
21157
+ this.analytics.logError(interpreted.message, {
21158
+ source: 'handled',
21159
+ code: interpreted.code,
21160
+ ...(context ? { context } : {}),
21161
+ ...(interpreted.status !== undefined ? { status: String(interpreted.status) } : {}),
21162
+ ...(interpreted.operationId ? { operationId: interpreted.operationId } : {}),
21163
+ ...(interpreted.isNetwork ? { error_category: 'network' } : {}),
21164
+ });
21165
+ }
21166
+ catch (trackingError) {
21167
+ console.warn('[ValtechError] analytics tracking failed:', trackingError);
21168
+ }
21169
+ }
21170
+ // 3. Resolver mensaje localizado para el usuario.
21171
+ const message = this.resolveMessage(interpreted, {
21172
+ i18nMap,
21173
+ fallbackKey,
21174
+ i18nNamespace,
21175
+ });
21176
+ // 4. Toast (estándar Valtech: color 'dark', position 'top').
21177
+ if (toast !== false) {
21178
+ this.toast.show({
21179
+ message,
21180
+ color: 'dark',
21181
+ position: 'top',
21182
+ duration: 3000,
21183
+ });
21184
+ }
21185
+ // 5. Devolver el error normalizado para lógica extra del caller.
21186
+ return interpreted;
21187
+ }
21188
+ /**
21189
+ * Resuelve el mensaje a mostrar siguiendo el orden documentado en {@link handle}.
21190
+ */
21191
+ resolveMessage(interpreted, opts) {
21192
+ const ns = opts.i18nNamespace;
21193
+ // 1. Match exacto por código de backend.
21194
+ const mappedKey = opts.i18nMap?.[interpreted.code];
21195
+ if (mappedKey) {
21196
+ return this.translate(mappedKey, ns) ?? interpreted.message;
21197
+ }
21198
+ // 2. Fallo de red → key de red de la lib, con fallback español.
21199
+ if (interpreted.isNetwork) {
21200
+ return this.translate(VALTECH_NETWORK_ERROR_KEY, ns) ?? NETWORK_FALLBACK_MESSAGE;
21201
+ }
21202
+ // 3. fallbackKey provisto por la app.
21203
+ if (opts.fallbackKey) {
21204
+ return this.translate(opts.fallbackKey, ns) ?? interpreted.message;
21205
+ }
21206
+ // 4. Genérico — preferir el mensaje del backend (ya viene en español) si lo hay.
21207
+ return interpreted.message || GENERIC_FALLBACK_MESSAGE;
21208
+ }
21209
+ /**
21210
+ * Traduce una key i18n. Devuelve `null` si la key no está definida — el
21211
+ * `I18nService` devuelve un placeholder `[namespace.key]` para keys faltantes;
21212
+ * lo detectamos para poder caer a un fallback en lugar de mostrar el placeholder.
21213
+ */
21214
+ translate(key, namespace) {
21215
+ const text = this.i18n.t(key, namespace);
21216
+ const ns = namespace ?? '_global';
21217
+ return text === `[${ns}.${key}]` ? null : text;
21218
+ }
21219
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ValtechErrorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
21220
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ValtechErrorService, providedIn: 'root' }); }
21221
+ }
21222
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ValtechErrorService, decorators: [{
21223
+ type: Injectable,
21224
+ args: [{ providedIn: 'root' }]
21225
+ }] });
21226
+
21227
+ /**
21228
+ * Estándar de manejo de errores del factory Valtech.
21229
+ *
21230
+ * - `interpretError` (Capa 0) — normaliza cualquier error a `InterpretedError`.
21231
+ * Función pura, sin Angular DI.
21232
+ * - `provideValtechErrorHandling` (Capa 1) — registra el interceptor HTTP de
21233
+ * observabilidad: log estructurado + reporte a Analytics, re-lanza el error.
21234
+ * - `ValtechErrorService` (Capa 3) — punto único para el `catch` de una página:
21235
+ * normaliza, observa, resuelve un mensaje i18n y muestra un toast.
21236
+ */
21237
+
20784
21238
  /**
20785
21239
  * Tipos e interfaces para el servicio de autenticación de Valtech.
20786
21240
  * Alineados con el backend AuthV2.
@@ -25793,9 +26247,13 @@ class AuthService {
25793
26247
  }
25794
26248
  }
25795
26249
  handleAuthError(error) {
26250
+ // `interpretError` (helper compartido de la lib) normaliza el
26251
+ // HttpErrorResponse — incluyendo fallos de red (status 0). Aplanamos a
26252
+ // AuthError para mantener la API pública de AuthService estable.
26253
+ const interpreted = interpretError(error);
25796
26254
  const authError = {
25797
- code: error.error?.code || 'UNKNOWN_ERROR',
25798
- message: error.error?.message || 'Error de autenticación desconocido',
26255
+ code: interpreted.code,
26256
+ message: interpreted.message,
25799
26257
  };
25800
26258
  this.stateService.setError(authError);
25801
26259
  return throwError(() => authError);
@@ -29261,56 +29719,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
29261
29719
  */
29262
29720
  // Tipos
29263
29721
 
29264
- /**
29265
- * Service for displaying toast notifications using Ionic's ToastController.
29266
- * Provides methods to show and present toasts with custom options.
29267
- */
29268
- class ToastService {
29269
- constructor(toastController) {
29270
- this.toastController = toastController;
29271
- }
29272
- /**
29273
- * Presents a toast notification with the given options.
29274
- *
29275
- * Estándar Valtech: todos los toasts son `color: 'dark'` y `position: 'top'`.
29276
- * Estos son los defaults cuando el caller no los especifica — no hace falta
29277
- * pasarlos en cada llamada. El diferenciador semántico (éxito/error) va en
29278
- * el mensaje, no en el color.
29279
- *
29280
- * @param request Toast options (message, duration, position, color, etc.)
29281
- */
29282
- async presentToast(request) {
29283
- const toast = await this.toastController.create({
29284
- message: request.message,
29285
- duration: request.duration,
29286
- position: request.position ?? 'top',
29287
- color: request.color ?? 'dark',
29288
- });
29289
- await toast.present();
29290
- }
29291
- /**
29292
- * Shows a toast notification and logs the result.
29293
- * @param request Toast options (message, duration, position, color, etc.)
29294
- */
29295
- show(request) {
29296
- this.presentToast(request)
29297
- .then(() => {
29298
- console.info('Toast created');
29299
- })
29300
- .catch(error => {
29301
- console.error(JSON.stringify(error));
29302
- });
29303
- }
29304
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, deps: [{ token: i2.ToastController }], target: i0.ɵɵFactoryTarget.Injectable }); }
29305
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, providedIn: 'root' }); }
29306
- }
29307
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, decorators: [{
29308
- type: Injectable,
29309
- args: [{
29310
- providedIn: 'root',
29311
- }]
29312
- }], ctorParameters: () => [{ type: i2.ToastController }] });
29313
-
29314
29722
  /**
29315
29723
  * `val-change-password-modal` — modal de gestión de contraseña para un usuario
29316
29724
  * autenticado. Análogo al modal de "recuperar contraseña" del `val-login`.
@@ -29337,20 +29745,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
29337
29745
  * ```
29338
29746
  */
29339
29747
  class ChangePasswordModalComponent {
29340
- /**
29341
- * Controla la visibilidad del modal. Lo decide el componente padre. Cada vez
29342
- * que pasa de cerrado a abierto se resuelve el modo (change vs set).
29343
- */
29344
- set isOpen(value) {
29345
- const opening = value && !this._isOpen;
29346
- this._isOpen = value;
29347
- if (opening) {
29348
- this.resolveMode();
29349
- }
29350
- }
29351
- get isOpen() {
29352
- return this._isOpen;
29353
- }
29354
29748
  constructor() {
29355
29749
  this._isOpen = false;
29356
29750
  /** Emite al cambiar/crear la contraseña con éxito. El padre cierra el modal. */
@@ -29420,7 +29814,20 @@ class ChangePasswordModalComponent {
29420
29814
  state: this._formState(),
29421
29815
  });
29422
29816
  });
29423
- addIcons({ closeOutline });
29817
+ }
29818
+ /**
29819
+ * Controla la visibilidad del modal. Lo decide el componente padre. Cada vez
29820
+ * que pasa de cerrado a abierto se resuelve el modo (change vs set).
29821
+ */
29822
+ set isOpen(value) {
29823
+ const opening = value && !this._isOpen;
29824
+ this._isOpen = value;
29825
+ if (opening) {
29826
+ this.resolveMode();
29827
+ }
29828
+ }
29829
+ get isOpen() {
29830
+ return this._isOpen;
29424
29831
  }
29425
29832
  /** Traduce una clave del namespace `_auth`. */
29426
29833
  t(key) {
@@ -29537,12 +29944,12 @@ class ChangePasswordModalComponent {
29537
29944
  this.toast.show({ message, duration: 3500 });
29538
29945
  }
29539
29946
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
29540
- 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"] }] }); }
29947
+ 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\" color=\"dark\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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: 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"] }] }); }
29541
29948
  }
29542
29949
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, decorators: [{
29543
29950
  type: Component,
29544
- 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"] }]
29545
- }], ctorParameters: () => [], propDecorators: { isOpen: [{
29951
+ args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, 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\" color=\"dark\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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"] }]
29952
+ }], propDecorators: { isOpen: [{
29546
29953
  type: Input
29547
29954
  }], changed: [{
29548
29955
  type: Output
@@ -30602,7 +31009,7 @@ class MfaModalComponent {
30602
31009
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
30603
31010
  }));
30604
31011
  this.resendTimer = null;
30605
- addIcons({ closeOutline, informationCircleOutline });
31012
+ addIcons({ informationCircleOutline });
30606
31013
  }
30607
31014
  ngOnDestroy() {
30608
31015
  this.stopCooldown();
@@ -30906,7 +31313,7 @@ class MfaModalComponent {
30906
31313
  this.toast.show({ message, duration: 3500 });
30907
31314
  }
30908
31315
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
30909
- 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"] }] }); }
31316
+ 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\" color=\"dark\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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"] }] }); }
30910
31317
  }
30911
31318
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
30912
31319
  type: Component,
@@ -30928,7 +31335,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
30928
31335
  FormComponent,
30929
31336
  QrCodeComponent,
30930
31337
  PinInputComponent,
30931
- ], 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"] }]
31338
+ ], 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\" color=\"dark\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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"] }]
30932
31339
  }], ctorParameters: () => [], propDecorators: { isOpen: [{
30933
31340
  type: Input
30934
31341
  }], prefillCode: [{
@@ -47382,5 +47789,5 @@ function buildFooterLinks(links, t, resolver) {
47382
47789
  * Generated bundle index. Do not edit.
47383
47790
  */
47384
47791
 
47385
- 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 };
47792
+ 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_NETWORK_ERROR_KEY, VALTECH_SOCIAL_LINKS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
47386
47793
  //# sourceMappingURL=valtech-components.mjs.map