valtech-components 2.0.838 → 2.0.840
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/lib/components/molecules/update-banner/types.mjs +4 -4
- package/esm2022/lib/components/molecules/update-banner/update-banner.component.mjs +5 -5
- package/esm2022/lib/components/organisms/change-password-modal/change-password-modal.component.mjs +19 -22
- package/esm2022/lib/components/organisms/mfa-modal/mfa-modal.component.mjs +5 -5
- package/esm2022/lib/config/company-footer.config.mjs +10 -3
- package/esm2022/lib/services/auth/auth.service.mjs +8 -3
- package/esm2022/lib/services/errors/index.mjs +9 -0
- package/esm2022/lib/services/errors/interpret-error.mjs +130 -0
- package/esm2022/lib/services/firebase/index.mjs +2 -2
- package/esm2022/lib/services/firebase/messaging.service.mjs +212 -3
- package/esm2022/lib/services/firebase/types.mjs +1 -1
- package/esm2022/lib/services/i18n/default-content.mjs +5 -1
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +6 -1
- package/fesm2022/valtech-components.mjs +397 -36
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/molecules/update-banner/types.d.ts +3 -3
- package/lib/components/organisms/change-password-modal/change-password-modal.component.d.ts +0 -1
- package/lib/config/company-footer.config.d.ts +4 -0
- package/lib/services/errors/index.d.ts +9 -0
- package/lib/services/errors/interpret-error.d.ts +63 -0
- package/lib/services/firebase/index.d.ts +1 -1
- package/lib/services/firebase/messaging.service.d.ts +110 -1
- package/lib/services/firebase/types.d.ts +30 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
|
@@ -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.
|
|
56
|
+
const VERSION = '2.0.840';
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Servicio para gestionar presets de componentes.
|
|
@@ -4412,6 +4412,8 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
4412
4412
|
termsAndConditions: 'Términos y Condiciones',
|
|
4413
4413
|
and: 'y',
|
|
4414
4414
|
privacyPolicy: 'Política de Privacidad',
|
|
4415
|
+
// Acciones genéricas
|
|
4416
|
+
close: 'Cerrar',
|
|
4415
4417
|
// Toasts
|
|
4416
4418
|
welcome: '¡Bienvenido!',
|
|
4417
4419
|
completeAllFields: 'Completa todos los campos.',
|
|
@@ -4566,6 +4568,8 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
4566
4568
|
termsAndConditions: 'Terms and Conditions',
|
|
4567
4569
|
and: 'and',
|
|
4568
4570
|
privacyPolicy: 'Privacy Policy',
|
|
4571
|
+
// Acciones genéricas
|
|
4572
|
+
close: 'Close',
|
|
4569
4573
|
// Toasts
|
|
4570
4574
|
welcome: 'Welcome!',
|
|
4571
4575
|
completeAllFields: 'Complete all fields.',
|
|
@@ -20056,7 +20060,7 @@ const UPDATE_BANNER_I18N_NAMESPACE = 'UpdateBanner';
|
|
|
20056
20060
|
*/
|
|
20057
20061
|
const UPDATE_BANNER_DEFAULT_CONTENT = {
|
|
20058
20062
|
es: {
|
|
20059
|
-
availableTitle: 'Hay una versión nueva disponible',
|
|
20063
|
+
availableTitle: '✨ Hay una versión nueva disponible',
|
|
20060
20064
|
availableMessage: 'Actualiza para obtener las últimas mejoras.',
|
|
20061
20065
|
requiredTitle: 'Debes actualizar para continuar',
|
|
20062
20066
|
requiredMessage: 'Esta versión ya no es compatible. Actualiza para seguir.',
|
|
@@ -20064,7 +20068,7 @@ const UPDATE_BANNER_DEFAULT_CONTENT = {
|
|
|
20064
20068
|
dismissAction: 'Cerrar',
|
|
20065
20069
|
},
|
|
20066
20070
|
en: {
|
|
20067
|
-
availableTitle: 'A new version is available',
|
|
20071
|
+
availableTitle: '✨ A new version is available',
|
|
20068
20072
|
availableMessage: 'Update to get the latest improvements.',
|
|
20069
20073
|
requiredTitle: 'You must update to continue',
|
|
20070
20074
|
requiredMessage: 'This version is no longer supported. Please update.',
|
|
@@ -20072,7 +20076,7 @@ const UPDATE_BANNER_DEFAULT_CONTENT = {
|
|
|
20072
20076
|
dismissAction: 'Close',
|
|
20073
20077
|
},
|
|
20074
20078
|
pt: {
|
|
20075
|
-
availableTitle: 'Há uma nova versão disponível',
|
|
20079
|
+
availableTitle: '✨ Há uma nova versão disponível',
|
|
20076
20080
|
availableMessage: 'Atualize para obter as últimas melhorias.',
|
|
20077
20081
|
requiredTitle: 'Você precisa atualizar para continuar',
|
|
20078
20082
|
requiredMessage: 'Esta versão não é mais compatível. Atualize para seguir.',
|
|
@@ -20224,7 +20228,7 @@ class UpdateBannerComponent {
|
|
|
20224
20228
|
<val-button
|
|
20225
20229
|
[props]="{
|
|
20226
20230
|
text: t().dismissAction,
|
|
20227
|
-
color: '
|
|
20231
|
+
color: 'dark',
|
|
20228
20232
|
fill: 'clear',
|
|
20229
20233
|
size: 'small',
|
|
20230
20234
|
state: 'ENABLED',
|
|
@@ -20237,7 +20241,7 @@ class UpdateBannerComponent {
|
|
|
20237
20241
|
</div>
|
|
20238
20242
|
</div>
|
|
20239
20243
|
}
|
|
20240
|
-
`, isInline: true, styles: ["
|
|
20244
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:contents}.val-update-banner{position:fixed;top:0;left:0;right:0;z-index:1100;display:flex;justify-content:center;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;pointer-events:none}.val-update-banner__backdrop{position:fixed;inset:0;z-index:-1;background:#0000008c;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);pointer-events:auto}.val-update-banner__panel{pointer-events:auto;display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;max-width:640px;padding:12px 16px;background:var(--ion-background-color, #fff);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08));border-radius:var(--val-border-radius, 12px);box-shadow:0 4px 24px #0000001f;animation:val-update-banner-in .25s ease-out}.val-update-banner--required{align-items:center;height:100%}.val-update-banner--required .val-update-banner__panel{flex-direction:column;text-align:center;max-width:420px}.val-update-banner__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;background:color-mix(in srgb,var(--ion-color-primary) 14%,transparent)}.val-update-banner--required .val-update-banner__icon{background:color-mix(in srgb,var(--ion-color-warning) 16%,transparent)}.val-update-banner__body{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}.val-update-banner__actions{flex:0 0 auto;display:flex;flex-direction:row;align-items:center;gap:4px}.val-update-banner--required .val-update-banner__actions{margin-top:8px}@keyframes val-update-banner-in{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (max-width: 540px){.val-update-banner__panel{flex-direction:column;text-align:center}.val-update-banner__actions{width:100%;justify-content:center}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "val-icon", inputs: ["props"] }] }); }
|
|
20241
20245
|
}
|
|
20242
20246
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UpdateBannerComponent, decorators: [{
|
|
20243
20247
|
type: Component,
|
|
@@ -20299,7 +20303,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
20299
20303
|
<val-button
|
|
20300
20304
|
[props]="{
|
|
20301
20305
|
text: t().dismissAction,
|
|
20302
|
-
color: '
|
|
20306
|
+
color: 'dark',
|
|
20303
20307
|
fill: 'clear',
|
|
20304
20308
|
size: 'small',
|
|
20305
20309
|
state: 'ENABLED',
|
|
@@ -20312,7 +20316,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
20312
20316
|
</div>
|
|
20313
20317
|
</div>
|
|
20314
20318
|
}
|
|
20315
|
-
`, styles: ["
|
|
20319
|
+
`, styles: ["@charset \"UTF-8\";:host{display:contents}.val-update-banner{position:fixed;top:0;left:0;right:0;z-index:1100;display:flex;justify-content:center;padding:calc(12px + env(safe-area-inset-top,0px)) 16px 12px;pointer-events:none}.val-update-banner__backdrop{position:fixed;inset:0;z-index:-1;background:#0000008c;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);pointer-events:auto}.val-update-banner__panel{pointer-events:auto;display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;max-width:640px;padding:12px 16px;background:var(--ion-background-color, #fff);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08));border-radius:var(--val-border-radius, 12px);box-shadow:0 4px 24px #0000001f;animation:val-update-banner-in .25s ease-out}.val-update-banner--required{align-items:center;height:100%}.val-update-banner--required .val-update-banner__panel{flex-direction:column;text-align:center;max-width:420px}.val-update-banner__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:12px;background:color-mix(in srgb,var(--ion-color-primary) 14%,transparent)}.val-update-banner--required .val-update-banner__icon{background:color-mix(in srgb,var(--ion-color-warning) 16%,transparent)}.val-update-banner__body{flex:1 1 auto;display:flex;flex-direction:column;gap:2px;min-width:0}.val-update-banner__actions{flex:0 0 auto;display:flex;flex-direction:row;align-items:center;gap:4px}.val-update-banner--required .val-update-banner__actions{margin-top:8px}@keyframes val-update-banner-in{0%{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}@media (max-width: 540px){.val-update-banner__panel{flex-direction:column;text-align:center}.val-update-banner__actions{width:100%;justify-content:center}}\n"] }]
|
|
20316
20320
|
}] });
|
|
20317
20321
|
|
|
20318
20322
|
/**
|
|
@@ -20781,6 +20785,144 @@ function provideValtechAuthInterceptor() {
|
|
|
20781
20785
|
return makeEnvironmentProviders([provideHttpClient(withInterceptors([authInterceptor]))]);
|
|
20782
20786
|
}
|
|
20783
20787
|
|
|
20788
|
+
/**
|
|
20789
|
+
* Error interpretation helper for the Valtech factory.
|
|
20790
|
+
*
|
|
20791
|
+
* Todos los frontends del factory consumen la misma API (backend Go) y la misma
|
|
20792
|
+
* librería. La lógica de interpretar errores debe vivir una sola vez, acá.
|
|
20793
|
+
*
|
|
20794
|
+
* El backend Go (`apperrors`) SIEMPRE devuelve errores como JSON:
|
|
20795
|
+
* { "code": string, "message": string, "operationId": string }
|
|
20796
|
+
* donde `message` ya viene en español y es user-friendly.
|
|
20797
|
+
*
|
|
20798
|
+
* En Angular un error HTTP llega como `HttpErrorResponse` (body en `.error`).
|
|
20799
|
+
* Un fallo de red es un `HttpErrorResponse` con `status === 0`.
|
|
20800
|
+
*
|
|
20801
|
+
* Además `AuthService.handleAuthError` aplana el `HttpErrorResponse` a un
|
|
20802
|
+
* `AuthError { code, message }` (code/message al nivel superior). Por eso este
|
|
20803
|
+
* helper acepta AMBAS formas — el crudo y el aplanado.
|
|
20804
|
+
*/
|
|
20805
|
+
/** Mensaje genérico para fallos de red (sin conexión / backend inalcanzable). */
|
|
20806
|
+
const NETWORK_MESSAGE = 'Sin conexión. Verifica tu conexión a internet e inténtalo de nuevo.';
|
|
20807
|
+
/** Mensaje genérico para errores no identificables. */
|
|
20808
|
+
const UNKNOWN_MESSAGE = 'Ocurrió un error inesperado. Inténtalo de nuevo.';
|
|
20809
|
+
/** Sentinel para fallos de red. */
|
|
20810
|
+
const NETWORK_CODE = 'NETWORK';
|
|
20811
|
+
/** Sentinel para errores no identificables. */
|
|
20812
|
+
const UNKNOWN_CODE = 'UNKNOWN';
|
|
20813
|
+
/** Type guard laxo: ¿el valor parece un `HttpErrorResponse`? */
|
|
20814
|
+
function isHttpErrorResponse(err) {
|
|
20815
|
+
return (typeof err === 'object' &&
|
|
20816
|
+
err !== null &&
|
|
20817
|
+
// No importamos HttpErrorResponse para mantener la fn libre de Angular;
|
|
20818
|
+
// detectamos por shape: tiene `status` numérico y `name` reconocible o `error`.
|
|
20819
|
+
('status' in err || err.name === 'HttpErrorResponse'));
|
|
20820
|
+
}
|
|
20821
|
+
/** Devuelve un string si el valor lo es y no está vacío; si no, `undefined`. */
|
|
20822
|
+
function asNonEmptyString(value) {
|
|
20823
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
20824
|
+
}
|
|
20825
|
+
/**
|
|
20826
|
+
* Normaliza CUALQUIER error a un `InterpretedError`.
|
|
20827
|
+
*
|
|
20828
|
+
* Función pura — sin dependencias de Angular DI, testeable y usable desde
|
|
20829
|
+
* cualquier lado (componentes, servicios, interceptores, scripts).
|
|
20830
|
+
*
|
|
20831
|
+
* Nunca lanza: siempre devuelve un `InterpretedError` válido.
|
|
20832
|
+
*
|
|
20833
|
+
* Casos cubiertos:
|
|
20834
|
+
* - `HttpErrorResponse` con `status === 0` (o sin respuesta) → fallo de red.
|
|
20835
|
+
* - `HttpErrorResponse` con body `{ code, message, operationId }` del backend.
|
|
20836
|
+
* - `AuthError` aplanado `{ code, message }` (code/message top-level).
|
|
20837
|
+
* - `Error` plano de JS → `code: 'UNKNOWN'`, `message: err.message`.
|
|
20838
|
+
* - Cualquier otra cosa (string, null, undefined, objeto raro) → genérico.
|
|
20839
|
+
*
|
|
20840
|
+
* @example
|
|
20841
|
+
* ```ts
|
|
20842
|
+
* try {
|
|
20843
|
+
* await firstValueFrom(this.http.get(url));
|
|
20844
|
+
* } catch (err) {
|
|
20845
|
+
* const e = interpretError(err);
|
|
20846
|
+
* if (e.isNetwork) {
|
|
20847
|
+
* this.toast.show({ message: e.message, color: 'dark' });
|
|
20848
|
+
* } else {
|
|
20849
|
+
* this.errorCode.set(e.code);
|
|
20850
|
+
* }
|
|
20851
|
+
* }
|
|
20852
|
+
* ```
|
|
20853
|
+
*/
|
|
20854
|
+
function interpretError(err) {
|
|
20855
|
+
// 1. HttpErrorResponse — el caso más común al hablar con el backend.
|
|
20856
|
+
if (isHttpErrorResponse(err)) {
|
|
20857
|
+
const status = typeof err.status === 'number' ? err.status : undefined;
|
|
20858
|
+
// 1a. Fallo de red: status 0 o sin body de respuesta del servidor.
|
|
20859
|
+
if (status === 0) {
|
|
20860
|
+
return {
|
|
20861
|
+
code: NETWORK_CODE,
|
|
20862
|
+
message: NETWORK_MESSAGE,
|
|
20863
|
+
status: 0,
|
|
20864
|
+
isNetwork: true,
|
|
20865
|
+
};
|
|
20866
|
+
}
|
|
20867
|
+
// 1b. Body del backend `{ code, message, operationId }`.
|
|
20868
|
+
const body = err.error;
|
|
20869
|
+
if (typeof body === 'object' && body !== null) {
|
|
20870
|
+
const b = body;
|
|
20871
|
+
return {
|
|
20872
|
+
code: asNonEmptyString(b.code) ?? UNKNOWN_CODE,
|
|
20873
|
+
message: asNonEmptyString(b.message) ?? UNKNOWN_MESSAGE,
|
|
20874
|
+
operationId: asNonEmptyString(b.operationId),
|
|
20875
|
+
status,
|
|
20876
|
+
isNetwork: false,
|
|
20877
|
+
};
|
|
20878
|
+
}
|
|
20879
|
+
// 1c. HttpErrorResponse sin body estructurado (ej. body string / null).
|
|
20880
|
+
return {
|
|
20881
|
+
code: UNKNOWN_CODE,
|
|
20882
|
+
message: asNonEmptyString(err.message) ?? UNKNOWN_MESSAGE,
|
|
20883
|
+
status,
|
|
20884
|
+
isNetwork: false,
|
|
20885
|
+
};
|
|
20886
|
+
}
|
|
20887
|
+
// 2. AuthError aplanado u objeto con `code`/`message` top-level.
|
|
20888
|
+
if (typeof err === 'object' && err !== null) {
|
|
20889
|
+
const o = err;
|
|
20890
|
+
const code = asNonEmptyString(o.code);
|
|
20891
|
+
const message = asNonEmptyString(o.message);
|
|
20892
|
+
if (code || message) {
|
|
20893
|
+
return {
|
|
20894
|
+
code: code ?? UNKNOWN_CODE,
|
|
20895
|
+
message: message ?? UNKNOWN_MESSAGE,
|
|
20896
|
+
operationId: asNonEmptyString(o.operationId),
|
|
20897
|
+
isNetwork: false,
|
|
20898
|
+
};
|
|
20899
|
+
}
|
|
20900
|
+
// 2b. Error plano de JS (instancia de Error sin code) — `message` ya
|
|
20901
|
+
// cubierto arriba; este branch atrapa Error con message vacío.
|
|
20902
|
+
if (err instanceof Error) {
|
|
20903
|
+
return {
|
|
20904
|
+
code: UNKNOWN_CODE,
|
|
20905
|
+
message: asNonEmptyString(err.message) ?? UNKNOWN_MESSAGE,
|
|
20906
|
+
isNetwork: false,
|
|
20907
|
+
};
|
|
20908
|
+
}
|
|
20909
|
+
}
|
|
20910
|
+
// 3. Cualquier otra cosa: string, null, undefined, objeto raro.
|
|
20911
|
+
return {
|
|
20912
|
+
code: UNKNOWN_CODE,
|
|
20913
|
+
message: UNKNOWN_MESSAGE,
|
|
20914
|
+
isNetwork: false,
|
|
20915
|
+
};
|
|
20916
|
+
}
|
|
20917
|
+
|
|
20918
|
+
/**
|
|
20919
|
+
* Error interpretation helpers.
|
|
20920
|
+
*
|
|
20921
|
+
* `interpretError` normaliza cualquier error (HttpErrorResponse crudo,
|
|
20922
|
+
* AuthError aplanado, Error de JS, o un valor arbitrario) a un
|
|
20923
|
+
* `InterpretedError` con forma estable. Función pura, sin Angular DI.
|
|
20924
|
+
*/
|
|
20925
|
+
|
|
20784
20926
|
/**
|
|
20785
20927
|
* Tipos e interfaces para el servicio de autenticación de Valtech.
|
|
20786
20928
|
* Alineados con el backend AuthV2.
|
|
@@ -23242,6 +23384,31 @@ class MessagingService {
|
|
|
23242
23384
|
* Es un *optimistic hint* — la verdad la confirma el siguiente `getToken()`.
|
|
23243
23385
|
*/
|
|
23244
23386
|
this.TOKEN_STORAGE_KEY = 'valtech_fcm_token';
|
|
23387
|
+
/**
|
|
23388
|
+
* Timeout (ms) para `navigator.serviceWorker.ready` dentro de `getToken()`.
|
|
23389
|
+
*
|
|
23390
|
+
* En un cold load el SW puede no estar activado aún y `serviceWorker.ready`
|
|
23391
|
+
* no resolver nunca. Pasado este tiempo, `getToken()` rechaza limpio en lugar
|
|
23392
|
+
* de colgarse indefinidamente.
|
|
23393
|
+
*/
|
|
23394
|
+
this.SW_READY_TIMEOUT_MS = 10_000;
|
|
23395
|
+
/**
|
|
23396
|
+
* Timeout (ms) del watchdog de `enable()` antes de auto-recargar la página.
|
|
23397
|
+
*
|
|
23398
|
+
* El flujo de activación a veces se cuelga ANTES de `getToken()` — caso
|
|
23399
|
+
* típico: `Notification.requestPermission()` no muestra el popup del SO en un
|
|
23400
|
+
* cold load. El timeout de `getToken()` (SW_READY_TIMEOUT_MS) no cubre eso;
|
|
23401
|
+
* por eso `enable()` envuelve el flujo completo en este watchdog.
|
|
23402
|
+
*
|
|
23403
|
+
* Un flujo exitoso real llega a token+device en ~4s; 15s es holgado y no
|
|
23404
|
+
* atrapa un éxito lento.
|
|
23405
|
+
*/
|
|
23406
|
+
this.ENABLE_WATCHDOG_MS = 15_000;
|
|
23407
|
+
/**
|
|
23408
|
+
* Key de sessionStorage — garantiza un solo auto-reload por sesión. Si el
|
|
23409
|
+
* flujo se cuelga una 2ª vez tras el reload, NO se recarga de nuevo (anti-loop).
|
|
23410
|
+
*/
|
|
23411
|
+
this.AUTORELOAD_FLAG = 'notif-enable-autoreload';
|
|
23245
23412
|
this.debugPersistence = this.config?.debugMessagePersistence ?? false;
|
|
23246
23413
|
this.initializeMessaging();
|
|
23247
23414
|
}
|
|
@@ -23395,6 +23562,158 @@ class MessagingService {
|
|
|
23395
23562
|
return null;
|
|
23396
23563
|
}
|
|
23397
23564
|
}
|
|
23565
|
+
/**
|
|
23566
|
+
* Flujo completo de activación de push, robusto, cross-app.
|
|
23567
|
+
*
|
|
23568
|
+
* Orquesta: pedir permiso → SW ready → obtener token FCM → (opcional)
|
|
23569
|
+
* registrar el device en backend. Es el punto de orquestación único que cada
|
|
23570
|
+
* app del factory consume — la lógica de robustez vive aquí, no en cada página.
|
|
23571
|
+
*
|
|
23572
|
+
* **Watchdog de auto-reload.** El flujo a veces se cuelga ANTES de `getToken()`
|
|
23573
|
+
* (ej. `Notification.requestPermission()` que no muestra el popup en un cold
|
|
23574
|
+
* load) — el timeout interno de `getToken()` no cubre ese caso. Por eso
|
|
23575
|
+
* `enable()` envuelve el flujo entero en un watchdog: si no alcanza un estado
|
|
23576
|
+
* terminal en `ENABLE_WATCHDOG_MS` (15s):
|
|
23577
|
+
* - 1ª vez en la sesión → marca un flag en `sessionStorage` y hace
|
|
23578
|
+
* `window.location.reload()` (un fresh load suele tener el SW activo).
|
|
23579
|
+
* Resuelve con `{ status: 'timeout', reloaded: true }`.
|
|
23580
|
+
* - ya se auto-recargó antes → NO recarga (anti-loop): limpia el flag y
|
|
23581
|
+
* resuelve con `{ status: 'timeout', reloaded: false }` para que la app
|
|
23582
|
+
* muestre un error.
|
|
23583
|
+
*
|
|
23584
|
+
* NO hace throw — siempre resuelve con un `EnablePushResult` descriptivo que
|
|
23585
|
+
* la página consumidora inspecciona para decidir qué toast mostrar.
|
|
23586
|
+
*
|
|
23587
|
+
* @param options.registerDevice Callback opcional que registra el device en
|
|
23588
|
+
* el backend (vive en `AuthService` — se pasa como callback para evitar el
|
|
23589
|
+
* ciclo de DI AuthService ↔ MessagingService).
|
|
23590
|
+
*
|
|
23591
|
+
* @example
|
|
23592
|
+
* ```typescript
|
|
23593
|
+
* const result = await messaging.enable({
|
|
23594
|
+
* registerDevice: (token) => auth.registerDevice(token).then(r => r.registered),
|
|
23595
|
+
* });
|
|
23596
|
+
* if (result.status === 'enabled') {
|
|
23597
|
+
* // result.token disponible — push activo
|
|
23598
|
+
* } else if (result.status === 'timeout' && !result.reloaded) {
|
|
23599
|
+
* // mostrar error: el flujo se colgó y ya se consumió el auto-reload
|
|
23600
|
+
* }
|
|
23601
|
+
* ```
|
|
23602
|
+
*/
|
|
23603
|
+
async enable(options = {}) {
|
|
23604
|
+
console.log('[Messaging] enable() start');
|
|
23605
|
+
let watchdog;
|
|
23606
|
+
// El watchdog corre en paralelo al flujo real. Lo que gane el race define
|
|
23607
|
+
// el resultado. El flujo real, si gana, limpia el watchdog en el `finally`.
|
|
23608
|
+
const watchdogPromise = new Promise(resolve => {
|
|
23609
|
+
if (typeof window === 'undefined')
|
|
23610
|
+
return; // SSR — sin watchdog.
|
|
23611
|
+
watchdog = setTimeout(() => {
|
|
23612
|
+
watchdog = undefined;
|
|
23613
|
+
console.warn(`[Messaging] enable() colgado >${this.ENABLE_WATCHDOG_MS}ms — watchdog`);
|
|
23614
|
+
if (this.hasAutoReloaded()) {
|
|
23615
|
+
// Anti-loop: ya recargamos una vez esta sesión y volvió a colgar.
|
|
23616
|
+
console.warn('[Messaging] auto-reload ya consumido — no se recarga de nuevo');
|
|
23617
|
+
this.clearAutoReloadFlag();
|
|
23618
|
+
resolve({
|
|
23619
|
+
status: 'timeout',
|
|
23620
|
+
reloaded: false,
|
|
23621
|
+
reason: `Flujo de activación colgado >${this.ENABLE_WATCHDOG_MS / 1000}s`,
|
|
23622
|
+
});
|
|
23623
|
+
return;
|
|
23624
|
+
}
|
|
23625
|
+
// 1ª vez: marcar y recargar. Un fresh load suele tener el SW activo.
|
|
23626
|
+
console.warn('[Messaging] auto-recargando la página (1/1 por sesión)');
|
|
23627
|
+
this.markAutoReloaded();
|
|
23628
|
+
resolve({
|
|
23629
|
+
status: 'timeout',
|
|
23630
|
+
reloaded: true,
|
|
23631
|
+
reason: 'Auto-recargando para reintentar la activación',
|
|
23632
|
+
});
|
|
23633
|
+
window.location.reload();
|
|
23634
|
+
}, this.ENABLE_WATCHDOG_MS);
|
|
23635
|
+
});
|
|
23636
|
+
const flow = this.runEnableFlow(options).finally(() => {
|
|
23637
|
+
if (watchdog !== undefined) {
|
|
23638
|
+
clearTimeout(watchdog);
|
|
23639
|
+
watchdog = undefined;
|
|
23640
|
+
}
|
|
23641
|
+
});
|
|
23642
|
+
return Promise.race([flow, watchdogPromise]);
|
|
23643
|
+
}
|
|
23644
|
+
/**
|
|
23645
|
+
* Flujo real de activación, sin el watchdog (lo envuelve `enable()`).
|
|
23646
|
+
* Resuelve siempre con un `EnablePushResult` — no hace throw.
|
|
23647
|
+
*/
|
|
23648
|
+
async runEnableFlow(options) {
|
|
23649
|
+
if (!(await this.isSupported())) {
|
|
23650
|
+
console.warn('[Messaging] enable: FCM no soportado');
|
|
23651
|
+
return { status: 'unsupported', reason: 'FCM no soportado en este navegador' };
|
|
23652
|
+
}
|
|
23653
|
+
try {
|
|
23654
|
+
// permission → SW ready → getToken (requestPermission encadena las 3).
|
|
23655
|
+
const token = await this.requestPermission();
|
|
23656
|
+
if (!token) {
|
|
23657
|
+
// requestPermission devuelve null tanto por permiso denegado como por
|
|
23658
|
+
// fallo al obtener el token. Distinguimos por el estado del permiso.
|
|
23659
|
+
const permission = this.getPermissionState();
|
|
23660
|
+
if (permission === 'denied' || permission === 'default') {
|
|
23661
|
+
return { status: 'denied', reason: 'Permiso de notificaciones denegado' };
|
|
23662
|
+
}
|
|
23663
|
+
return { status: 'error', reason: 'No se pudo obtener el token FCM' };
|
|
23664
|
+
}
|
|
23665
|
+
// Registro de device en backend (opcional — el caller pasa el callback).
|
|
23666
|
+
if (options.registerDevice) {
|
|
23667
|
+
const registered = await options.registerDevice(token);
|
|
23668
|
+
if (!registered) {
|
|
23669
|
+
return { status: 'error', token, reason: 'No se pudo registrar el dispositivo' };
|
|
23670
|
+
}
|
|
23671
|
+
}
|
|
23672
|
+
// Éxito: estado terminal. Limpiar el flag anti-loop para futuros intentos.
|
|
23673
|
+
this.clearAutoReloadFlag();
|
|
23674
|
+
console.log('[Messaging] enable() success');
|
|
23675
|
+
return { status: 'enabled', token };
|
|
23676
|
+
}
|
|
23677
|
+
catch (error) {
|
|
23678
|
+
const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
23679
|
+
console.error('[Messaging] enable() error:', reason, error);
|
|
23680
|
+
return { status: 'error', reason };
|
|
23681
|
+
}
|
|
23682
|
+
}
|
|
23683
|
+
/** True si ya se auto-recargó una vez en esta sesión. */
|
|
23684
|
+
hasAutoReloaded() {
|
|
23685
|
+
if (!isPlatformBrowser(this.platformId))
|
|
23686
|
+
return false;
|
|
23687
|
+
try {
|
|
23688
|
+
return sessionStorage.getItem(this.AUTORELOAD_FLAG) === '1';
|
|
23689
|
+
}
|
|
23690
|
+
catch {
|
|
23691
|
+
// sessionStorage puede no estar disponible (modo privado estricto, SSR).
|
|
23692
|
+
return false;
|
|
23693
|
+
}
|
|
23694
|
+
}
|
|
23695
|
+
/** Marca que se consumió el auto-reload de esta sesión. */
|
|
23696
|
+
markAutoReloaded() {
|
|
23697
|
+
if (!isPlatformBrowser(this.platformId))
|
|
23698
|
+
return;
|
|
23699
|
+
try {
|
|
23700
|
+
sessionStorage.setItem(this.AUTORELOAD_FLAG, '1');
|
|
23701
|
+
}
|
|
23702
|
+
catch {
|
|
23703
|
+
/* sessionStorage no disponible — best-effort. */
|
|
23704
|
+
}
|
|
23705
|
+
}
|
|
23706
|
+
/** Limpia el flag anti-loop — tras un éxito o un fallo definitivo. */
|
|
23707
|
+
clearAutoReloadFlag() {
|
|
23708
|
+
if (!isPlatformBrowser(this.platformId))
|
|
23709
|
+
return;
|
|
23710
|
+
try {
|
|
23711
|
+
sessionStorage.removeItem(this.AUTORELOAD_FLAG);
|
|
23712
|
+
}
|
|
23713
|
+
catch {
|
|
23714
|
+
/* sessionStorage no disponible — nada que limpiar. */
|
|
23715
|
+
}
|
|
23716
|
+
}
|
|
23398
23717
|
/**
|
|
23399
23718
|
* Obtiene el token FCM actual (sin solicitar permiso).
|
|
23400
23719
|
*
|
|
@@ -23423,8 +23742,11 @@ class MessagingService {
|
|
|
23423
23742
|
// revalidaciones del SW en iOS PWA. Solo registramos si no existe aún.
|
|
23424
23743
|
const registration = await this.resolveServiceWorkerRegistration();
|
|
23425
23744
|
console.log('[Messaging] SW resolved, waiting ready...');
|
|
23426
|
-
// Esperar a que el SW esté activo
|
|
23427
|
-
|
|
23745
|
+
// Esperar a que el SW esté activo, con timeout: `navigator.serviceWorker.ready`
|
|
23746
|
+
// puede no resolver NUNCA en un cold load (SW aún no activado) → sin el
|
|
23747
|
+
// timeout `getToken()` se cuelga indefinidamente. Con el race, si el SW no
|
|
23748
|
+
// queda listo en SW_READY_TIMEOUT_MS, esta promesa rechaza limpio.
|
|
23749
|
+
await this.waitForServiceWorkerReady();
|
|
23428
23750
|
console.log('[Messaging] SW ready, calling Firebase getToken()...');
|
|
23429
23751
|
const token = await getToken(messaging, {
|
|
23430
23752
|
vapidKey,
|
|
@@ -23463,6 +23785,35 @@ class MessagingService {
|
|
|
23463
23785
|
console.warn('[Messaging] SW not registered yet, registering as fallback');
|
|
23464
23786
|
return navigator.serviceWorker.register('/firebase-messaging-sw.js');
|
|
23465
23787
|
}
|
|
23788
|
+
/**
|
|
23789
|
+
* Espera a que el Service Worker quede activo (`navigator.serviceWorker.ready`)
|
|
23790
|
+
* pero con un timeout duro.
|
|
23791
|
+
*
|
|
23792
|
+
* `navigator.serviceWorker.ready` resuelve cuando hay un SW *activo*. En un
|
|
23793
|
+
* cold load (primera visita, SW recién registrado, iOS PWA recién abierta) el
|
|
23794
|
+
* SW puede quedar en estado `installing`/`waiting` y `ready` no resolver nunca
|
|
23795
|
+
* → `getToken()` se cuelga. El `Promise.race` contra un `setTimeout` garantiza
|
|
23796
|
+
* que esta espera termina: o gana `ready` (caso normal) o gana el timeout y
|
|
23797
|
+
* lanzamos un error claro para que `getToken()` rechace en vez de colgarse.
|
|
23798
|
+
*
|
|
23799
|
+
* El timer se limpia en ambas ramas para no dejar timers colgando.
|
|
23800
|
+
*/
|
|
23801
|
+
waitForServiceWorkerReady() {
|
|
23802
|
+
let timer;
|
|
23803
|
+
const ready = navigator.serviceWorker.ready.then(() => {
|
|
23804
|
+
if (timer !== undefined)
|
|
23805
|
+
clearTimeout(timer);
|
|
23806
|
+
});
|
|
23807
|
+
const timeout = new Promise((_, reject) => {
|
|
23808
|
+
timer = setTimeout(() => {
|
|
23809
|
+
reject(new Error(`[Messaging] service worker no quedó listo en ${this.SW_READY_TIMEOUT_MS / 1000}s`));
|
|
23810
|
+
}, this.SW_READY_TIMEOUT_MS);
|
|
23811
|
+
});
|
|
23812
|
+
return Promise.race([ready, timeout]).finally(() => {
|
|
23813
|
+
if (timer !== undefined)
|
|
23814
|
+
clearTimeout(timer);
|
|
23815
|
+
});
|
|
23816
|
+
}
|
|
23466
23817
|
/**
|
|
23467
23818
|
* Persiste el token FCM en localStorage (o lo limpia si es null/empty).
|
|
23468
23819
|
*/
|
|
@@ -25584,9 +25935,13 @@ class AuthService {
|
|
|
25584
25935
|
}
|
|
25585
25936
|
}
|
|
25586
25937
|
handleAuthError(error) {
|
|
25938
|
+
// `interpretError` (helper compartido de la lib) normaliza el
|
|
25939
|
+
// HttpErrorResponse — incluyendo fallos de red (status 0). Aplanamos a
|
|
25940
|
+
// AuthError para mantener la API pública de AuthService estable.
|
|
25941
|
+
const interpreted = interpretError(error);
|
|
25587
25942
|
const authError = {
|
|
25588
|
-
code:
|
|
25589
|
-
message:
|
|
25943
|
+
code: interpreted.code,
|
|
25944
|
+
message: interpreted.message,
|
|
25590
25945
|
};
|
|
25591
25946
|
this.stateService.setError(authError);
|
|
25592
25947
|
return throwError(() => authError);
|
|
@@ -29128,20 +29483,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
29128
29483
|
* ```
|
|
29129
29484
|
*/
|
|
29130
29485
|
class ChangePasswordModalComponent {
|
|
29131
|
-
/**
|
|
29132
|
-
* Controla la visibilidad del modal. Lo decide el componente padre. Cada vez
|
|
29133
|
-
* que pasa de cerrado a abierto se resuelve el modo (change vs set).
|
|
29134
|
-
*/
|
|
29135
|
-
set isOpen(value) {
|
|
29136
|
-
const opening = value && !this._isOpen;
|
|
29137
|
-
this._isOpen = value;
|
|
29138
|
-
if (opening) {
|
|
29139
|
-
this.resolveMode();
|
|
29140
|
-
}
|
|
29141
|
-
}
|
|
29142
|
-
get isOpen() {
|
|
29143
|
-
return this._isOpen;
|
|
29144
|
-
}
|
|
29145
29486
|
constructor() {
|
|
29146
29487
|
this._isOpen = false;
|
|
29147
29488
|
/** Emite al cambiar/crear la contraseña con éxito. El padre cierra el modal. */
|
|
@@ -29211,7 +29552,20 @@ class ChangePasswordModalComponent {
|
|
|
29211
29552
|
state: this._formState(),
|
|
29212
29553
|
});
|
|
29213
29554
|
});
|
|
29214
|
-
|
|
29555
|
+
}
|
|
29556
|
+
/**
|
|
29557
|
+
* Controla la visibilidad del modal. Lo decide el componente padre. Cada vez
|
|
29558
|
+
* que pasa de cerrado a abierto se resuelve el modo (change vs set).
|
|
29559
|
+
*/
|
|
29560
|
+
set isOpen(value) {
|
|
29561
|
+
const opening = value && !this._isOpen;
|
|
29562
|
+
this._isOpen = value;
|
|
29563
|
+
if (opening) {
|
|
29564
|
+
this.resolveMode();
|
|
29565
|
+
}
|
|
29566
|
+
}
|
|
29567
|
+
get isOpen() {
|
|
29568
|
+
return this._isOpen;
|
|
29215
29569
|
}
|
|
29216
29570
|
/** Traduce una clave del namespace `_auth`. */
|
|
29217
29571
|
t(key) {
|
|
@@ -29328,12 +29682,12 @@ class ChangePasswordModalComponent {
|
|
|
29328
29682
|
this.toast.show({ message, duration: 3500 });
|
|
29329
29683
|
}
|
|
29330
29684
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
29331
|
-
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 <
|
|
29685
|
+
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"] }] }); }
|
|
29332
29686
|
}
|
|
29333
29687
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, decorators: [{
|
|
29334
29688
|
type: Component,
|
|
29335
|
-
args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader,
|
|
29336
|
-
}],
|
|
29689
|
+
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"] }]
|
|
29690
|
+
}], propDecorators: { isOpen: [{
|
|
29337
29691
|
type: Input
|
|
29338
29692
|
}], changed: [{
|
|
29339
29693
|
type: Output
|
|
@@ -30393,7 +30747,7 @@ class MfaModalComponent {
|
|
|
30393
30747
|
state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
30394
30748
|
}));
|
|
30395
30749
|
this.resendTimer = null;
|
|
30396
|
-
addIcons({
|
|
30750
|
+
addIcons({ informationCircleOutline });
|
|
30397
30751
|
}
|
|
30398
30752
|
ngOnDestroy() {
|
|
30399
30753
|
this.stopCooldown();
|
|
@@ -30697,7 +31051,7 @@ class MfaModalComponent {
|
|
|
30697
31051
|
this.toast.show({ message, duration: 3500 });
|
|
30698
31052
|
}
|
|
30699
31053
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30700
|
-
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"] }] }); }
|
|
31054
|
+
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"] }] }); }
|
|
30701
31055
|
}
|
|
30702
31056
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
|
|
30703
31057
|
type: Component,
|
|
@@ -30719,7 +31073,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
30719
31073
|
FormComponent,
|
|
30720
31074
|
QrCodeComponent,
|
|
30721
31075
|
PinInputComponent,
|
|
30722
|
-
], 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"] }]
|
|
31076
|
+
], 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"] }]
|
|
30723
31077
|
}], ctorParameters: () => [], propDecorators: { isOpen: [{
|
|
30724
31078
|
type: Input
|
|
30725
31079
|
}], prefillCode: [{
|
|
@@ -47070,10 +47424,13 @@ const VALTECH_FOOTER_LOGO = {
|
|
|
47070
47424
|
* Company links organized by section
|
|
47071
47425
|
*/
|
|
47072
47426
|
const VALTECH_COMPANY_LINKS = {
|
|
47427
|
+
// Links legales con `external: true` → abren en tab nueva (target=_blank).
|
|
47073
47428
|
legal: [
|
|
47074
47429
|
{ key: 'aboutUs', url: '/legal/about', kind: 'site', external: false },
|
|
47075
|
-
{ key: 'privacyPolicy', url: '/legal/privacy', kind: 'legal', external:
|
|
47076
|
-
{ key: 'termsConditions', url: '/legal/terms', kind: 'legal', external:
|
|
47430
|
+
{ key: 'privacyPolicy', url: '/legal/privacy', kind: 'legal', external: true },
|
|
47431
|
+
{ key: 'termsConditions', url: '/legal/terms', kind: 'legal', external: true },
|
|
47432
|
+
{ key: 'cookiesPolicy', url: '/legal/cookies', kind: 'legal', external: true },
|
|
47433
|
+
{ key: 'legalNotice', url: '/legal/legal-notice', kind: 'legal', external: true },
|
|
47077
47434
|
],
|
|
47078
47435
|
support: [
|
|
47079
47436
|
{ key: 'contactSupport', url: '/contact', kind: 'support', external: false },
|
|
@@ -47096,6 +47453,8 @@ const VALTECH_FOOTER_I18N = {
|
|
|
47096
47453
|
aboutUs: 'Nosotros',
|
|
47097
47454
|
privacyPolicy: 'Política de privacidad',
|
|
47098
47455
|
termsConditions: 'Términos y condiciones',
|
|
47456
|
+
cookiesPolicy: 'Política de cookies',
|
|
47457
|
+
legalNotice: 'Aviso legal',
|
|
47099
47458
|
// Support links
|
|
47100
47459
|
contactSupport: 'Contactar a soporte',
|
|
47101
47460
|
faq: 'Preguntas frecuentes',
|
|
@@ -47111,6 +47470,8 @@ const VALTECH_FOOTER_I18N = {
|
|
|
47111
47470
|
aboutUs: 'About Us',
|
|
47112
47471
|
privacyPolicy: 'Privacy Policy',
|
|
47113
47472
|
termsConditions: 'Terms & Conditions',
|
|
47473
|
+
cookiesPolicy: 'Cookie Policy',
|
|
47474
|
+
legalNotice: 'Legal Notice',
|
|
47114
47475
|
// Support links
|
|
47115
47476
|
contactSupport: 'Contact Support',
|
|
47116
47477
|
faq: 'FAQ',
|
|
@@ -47166,5 +47527,5 @@ function buildFooterLinks(links, t, resolver) {
|
|
|
47166
47527
|
* Generated bundle index. Do not edit.
|
|
47167
47528
|
*/
|
|
47168
47529
|
|
|
47169
|
-
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 };
|
|
47530
|
+
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, interpretError, 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 };
|
|
47170
47531
|
//# sourceMappingURL=valtech-components.mjs.map
|