valtech-components 4.0.56 → 4.0.57
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/organisms/login/login.component.mjs +188 -24
- package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +62 -2
- package/esm2022/lib/components/organisms/organization-view/organization-view.i18n.mjs +12 -6
- package/esm2022/lib/components/organisms/organization-view/types.mjs +1 -1
- package/esm2022/lib/services/auth/auth.service.mjs +20 -1
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/services/i18n/default-content.mjs +29 -1
- package/esm2022/lib/version.mjs +2 -2
- package/fesm2022/valtech-components.mjs +305 -30
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/login/login.component.d.ts +17 -0
- package/lib/components/organisms/organization-view/organization-view.component.d.ts +12 -1
- package/lib/components/organisms/organization-view/types.d.ts +7 -0
- package/lib/services/auth/auth.service.d.ts +12 -1
- package/lib/services/auth/types.d.ts +28 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
|
|
|
56
56
|
* Current version of valtech-components.
|
|
57
57
|
* This is automatically updated during the publish process.
|
|
58
58
|
*/
|
|
59
|
-
const VERSION = '4.0.
|
|
59
|
+
const VERSION = '4.0.57';
|
|
60
60
|
|
|
61
61
|
// Control de estado de refresco (singleton a nivel de módulo)
|
|
62
62
|
let isRefreshing = false;
|
|
@@ -7341,6 +7341,20 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
7341
7341
|
mfaTOTP: 'Ingresa el código de tu app de autenticación',
|
|
7342
7342
|
mfaEmail: 'Ingresa el código enviado a tu correo',
|
|
7343
7343
|
mfaSMS: 'Ingresa el código enviado a tu teléfono',
|
|
7344
|
+
mfaBackupCode: 'Ingresa uno de tus códigos de respaldo',
|
|
7345
|
+
mfaUseBackupLink: '¿Perdiste tu dispositivo?',
|
|
7346
|
+
mfaUseBackupAction: 'Usa un código de respaldo',
|
|
7347
|
+
mfaUseTotpAction: 'Volver al código de la app',
|
|
7348
|
+
backupCodeMinLength: 'El código de respaldo tiene 8 caracteres',
|
|
7349
|
+
mfaLostBackupLink: '¿Sin acceso a tu segundo factor?',
|
|
7350
|
+
mfaResetAction: 'Restablecer MFA por correo',
|
|
7351
|
+
mfaResetTitle: 'Restablecer MFA',
|
|
7352
|
+
mfaResetRequestDescription: 'Te enviaremos un código a tu correo para deshabilitar la verificación en dos pasos.',
|
|
7353
|
+
mfaResetConfirmDescription: 'Ingresa el código que enviamos a tu correo.',
|
|
7354
|
+
mfaResetSubmit: 'Enviar código',
|
|
7355
|
+
mfaResetConfirmSubmit: 'Deshabilitar MFA',
|
|
7356
|
+
mfaResetCodeSent: 'Si la cuenta existe, enviamos un código a tu correo.',
|
|
7357
|
+
mfaResetDone: 'MFA deshabilitado. Inicia sesión nuevamente.',
|
|
7344
7358
|
// Forgot/Reset password
|
|
7345
7359
|
forgotTitle: 'Recuperar contraseña',
|
|
7346
7360
|
forgotDescription: 'Ingresa tu correo electrónico y te enviaremos un código.',
|
|
@@ -7526,6 +7540,20 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
7526
7540
|
mfaTOTP: 'Enter the code from your authenticator app',
|
|
7527
7541
|
mfaEmail: 'Enter the code sent to your email',
|
|
7528
7542
|
mfaSMS: 'Enter the code sent to your phone',
|
|
7543
|
+
mfaBackupCode: 'Enter one of your backup codes',
|
|
7544
|
+
mfaUseBackupLink: 'Lost your device?',
|
|
7545
|
+
mfaUseBackupAction: 'Use a backup code',
|
|
7546
|
+
mfaUseTotpAction: 'Back to authenticator code',
|
|
7547
|
+
backupCodeMinLength: 'The backup code is 8 characters',
|
|
7548
|
+
mfaLostBackupLink: 'No access to your second factor?',
|
|
7549
|
+
mfaResetAction: 'Reset MFA by email',
|
|
7550
|
+
mfaResetTitle: 'Reset MFA',
|
|
7551
|
+
mfaResetRequestDescription: 'We will email you a code to disable two-step verification.',
|
|
7552
|
+
mfaResetConfirmDescription: 'Enter the code we sent to your email.',
|
|
7553
|
+
mfaResetSubmit: 'Send code',
|
|
7554
|
+
mfaResetConfirmSubmit: 'Disable MFA',
|
|
7555
|
+
mfaResetCodeSent: 'If the account exists, we sent a code to your email.',
|
|
7556
|
+
mfaResetDone: 'MFA disabled. Please sign in again.',
|
|
7529
7557
|
// Forgot/Reset password
|
|
7530
7558
|
forgotTitle: 'Recover password',
|
|
7531
7559
|
forgotDescription: 'Enter your email and we will send you a code.',
|
|
@@ -8762,6 +8790,25 @@ class AuthService {
|
|
|
8762
8790
|
.post(`${this.baseUrl}/reset-password`, request)
|
|
8763
8791
|
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8764
8792
|
}
|
|
8793
|
+
/**
|
|
8794
|
+
* Inicia el reset de MFA por email cuando el user perdió su 2º factor
|
|
8795
|
+
* (TOTP + backup codes). Envía un código de 6 dígitos al email verificado.
|
|
8796
|
+
* Response siempre genérica (anti-enumeración).
|
|
8797
|
+
*/
|
|
8798
|
+
requestMFAReset(request) {
|
|
8799
|
+
return this.http
|
|
8800
|
+
.post(`${this.baseUrl}/mfa/reset-request`, request)
|
|
8801
|
+
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8802
|
+
}
|
|
8803
|
+
/**
|
|
8804
|
+
* Confirma el reset de MFA con el código recibido por email → deshabilita MFA.
|
|
8805
|
+
* Tras esto el user inicia sesión normalmente (sin 2º factor) y re-enrola.
|
|
8806
|
+
*/
|
|
8807
|
+
confirmMFAReset(request) {
|
|
8808
|
+
return this.http
|
|
8809
|
+
.post(`${this.baseUrl}/mfa/reset`, request)
|
|
8810
|
+
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8811
|
+
}
|
|
8765
8812
|
/**
|
|
8766
8813
|
* Activa una cuenta pre-aprovisionada por una organización (ADR-023): define
|
|
8767
8814
|
* la contraseña con el token del enlace de email. En éxito el backend devuelve
|
|
@@ -40691,6 +40738,16 @@ class LoginComponent {
|
|
|
40691
40738
|
this._verifyFormSectionName = signal('');
|
|
40692
40739
|
this._mfaVerifyFormState = signal(ComponentStates.ENABLED);
|
|
40693
40740
|
this._mfaMethod = signal('TOTP');
|
|
40741
|
+
/** Método MFA pendiente — expuesto al template para gatear el link de backup (solo TOTP). */
|
|
40742
|
+
this.mfaMethod = this._mfaMethod.asReadonly();
|
|
40743
|
+
/** A: el user togglea a "código de respaldo" (8 chars alfanuméricos) cuando perdió el 2º factor. */
|
|
40744
|
+
this.mfaUseBackupCode = signal(false);
|
|
40745
|
+
// B: reset de MFA por email (cuando el user no tiene backup codes).
|
|
40746
|
+
this.isMFAResetModalOpen = false;
|
|
40747
|
+
this._mfaResetStep = signal('request');
|
|
40748
|
+
this.mfaResetStep = this._mfaResetStep.asReadonly();
|
|
40749
|
+
this._mfaResetFormState = signal(ComponentStates.ENABLED);
|
|
40750
|
+
this._mfaResetEmail = signal('');
|
|
40694
40751
|
this._forgotPasswordFormState = signal(ComponentStates.ENABLED);
|
|
40695
40752
|
this._resetPasswordFormState = signal(ComponentStates.ENABLED);
|
|
40696
40753
|
this._resetFormSectionName = signal('');
|
|
@@ -40857,7 +40914,51 @@ class LoginComponent {
|
|
|
40857
40914
|
// ==========================================
|
|
40858
40915
|
this.mfaVerifyFormProps = computed(() => {
|
|
40859
40916
|
const method = this._mfaMethod();
|
|
40860
|
-
const
|
|
40917
|
+
const useBackup = this.mfaUseBackupCode();
|
|
40918
|
+
const sectionName = useBackup
|
|
40919
|
+
? this.t('mfaBackupCode')
|
|
40920
|
+
: method === 'TOTP'
|
|
40921
|
+
? this.t('mfaTOTP')
|
|
40922
|
+
: method === 'EMAIL'
|
|
40923
|
+
? this.t('mfaEmail')
|
|
40924
|
+
: this.t('mfaSMS');
|
|
40925
|
+
// A: backup code = 8 chars alfanuméricos → input de texto (el PIN de 6
|
|
40926
|
+
// celdas numéricas no puede capturarlo). El backend rutea por longitud.
|
|
40927
|
+
const codeField = useBackup
|
|
40928
|
+
? {
|
|
40929
|
+
type: InputType.TEXT,
|
|
40930
|
+
label: '',
|
|
40931
|
+
name: 'code',
|
|
40932
|
+
token: 'mfa-backup',
|
|
40933
|
+
hint: '',
|
|
40934
|
+
placeholder: '',
|
|
40935
|
+
errorKeys: {
|
|
40936
|
+
required: 'codeRequired',
|
|
40937
|
+
minlength: 'backupCodeMinLength',
|
|
40938
|
+
},
|
|
40939
|
+
validators: [Validators.required, Validators.minLength(8)],
|
|
40940
|
+
order: 0,
|
|
40941
|
+
state: ComponentStates.ENABLED,
|
|
40942
|
+
autoFocus: true,
|
|
40943
|
+
}
|
|
40944
|
+
: {
|
|
40945
|
+
type: InputType.PIN_CODE,
|
|
40946
|
+
label: '',
|
|
40947
|
+
name: 'code',
|
|
40948
|
+
token: 'mfa-pin',
|
|
40949
|
+
hint: '',
|
|
40950
|
+
placeholder: '',
|
|
40951
|
+
errorKeys: {
|
|
40952
|
+
required: 'codeRequired',
|
|
40953
|
+
minlength: 'codeMinLength',
|
|
40954
|
+
},
|
|
40955
|
+
validators: [Validators.required, Validators.minLength(6)],
|
|
40956
|
+
order: 0,
|
|
40957
|
+
state: ComponentStates.ENABLED,
|
|
40958
|
+
length: 6,
|
|
40959
|
+
allowNumbersOnly: true,
|
|
40960
|
+
autoFocus: true,
|
|
40961
|
+
};
|
|
40861
40962
|
return this.i18nHelper.resolveForm({
|
|
40862
40963
|
nameKey: 'mfaTitle',
|
|
40863
40964
|
i18nNamespace: '_auth',
|
|
@@ -40865,26 +40966,7 @@ class LoginComponent {
|
|
|
40865
40966
|
{
|
|
40866
40967
|
name: sectionName,
|
|
40867
40968
|
order: 0,
|
|
40868
|
-
fields: [
|
|
40869
|
-
{
|
|
40870
|
-
type: InputType.PIN_CODE,
|
|
40871
|
-
label: '',
|
|
40872
|
-
name: 'code',
|
|
40873
|
-
token: 'mfa-pin',
|
|
40874
|
-
hint: '',
|
|
40875
|
-
placeholder: '',
|
|
40876
|
-
errorKeys: {
|
|
40877
|
-
required: 'codeRequired',
|
|
40878
|
-
minlength: 'codeMinLength',
|
|
40879
|
-
},
|
|
40880
|
-
validators: [Validators.required, Validators.minLength(6)],
|
|
40881
|
-
order: 0,
|
|
40882
|
-
state: ComponentStates.ENABLED,
|
|
40883
|
-
length: 6,
|
|
40884
|
-
allowNumbersOnly: true,
|
|
40885
|
-
autoFocus: true,
|
|
40886
|
-
},
|
|
40887
|
-
],
|
|
40969
|
+
fields: [codeField],
|
|
40888
40970
|
},
|
|
40889
40971
|
],
|
|
40890
40972
|
actions: {
|
|
@@ -40896,6 +40978,79 @@ class LoginComponent {
|
|
|
40896
40978
|
});
|
|
40897
40979
|
});
|
|
40898
40980
|
// ==========================================
|
|
40981
|
+
// MFA RESET FORM (computed) — B: recovery por email
|
|
40982
|
+
// ==========================================
|
|
40983
|
+
this.mfaResetRequestFormProps = computed(() => this.i18nHelper.resolveForm({
|
|
40984
|
+
nameKey: 'mfaResetTitle',
|
|
40985
|
+
i18nNamespace: '_auth',
|
|
40986
|
+
sections: [
|
|
40987
|
+
{
|
|
40988
|
+
name: this.t('mfaResetRequestDescription'),
|
|
40989
|
+
order: 0,
|
|
40990
|
+
fields: [
|
|
40991
|
+
{
|
|
40992
|
+
type: InputType.EMAIL,
|
|
40993
|
+
label: '',
|
|
40994
|
+
name: 'email',
|
|
40995
|
+
token: 'mfa-reset-email',
|
|
40996
|
+
hint: '',
|
|
40997
|
+
placeholderKey: 'emailPlaceholder',
|
|
40998
|
+
value: this._mfaResetEmail(),
|
|
40999
|
+
errorKeys: {
|
|
41000
|
+
required: 'emailRequired',
|
|
41001
|
+
email: 'emailInvalid',
|
|
41002
|
+
},
|
|
41003
|
+
validators: [Validators.required, Validators.email],
|
|
41004
|
+
order: 0,
|
|
41005
|
+
state: ComponentStates.ENABLED,
|
|
41006
|
+
},
|
|
41007
|
+
],
|
|
41008
|
+
},
|
|
41009
|
+
],
|
|
41010
|
+
actions: {
|
|
41011
|
+
...button({ text: '', type: 'submit', fill: 'solid', expand: 'block' }),
|
|
41012
|
+
token: 'mfa-reset-request-submit',
|
|
41013
|
+
textKey: 'mfaResetSubmit',
|
|
41014
|
+
},
|
|
41015
|
+
state: this._mfaResetFormState(),
|
|
41016
|
+
}));
|
|
41017
|
+
this.mfaResetConfirmFormProps = computed(() => this.i18nHelper.resolveForm({
|
|
41018
|
+
nameKey: 'mfaResetTitle',
|
|
41019
|
+
i18nNamespace: '_auth',
|
|
41020
|
+
sections: [
|
|
41021
|
+
{
|
|
41022
|
+
name: this.t('mfaResetConfirmDescription'),
|
|
41023
|
+
order: 0,
|
|
41024
|
+
fields: [
|
|
41025
|
+
{
|
|
41026
|
+
type: InputType.PIN_CODE,
|
|
41027
|
+
label: '',
|
|
41028
|
+
name: 'code',
|
|
41029
|
+
token: 'mfa-reset-pin',
|
|
41030
|
+
hint: '',
|
|
41031
|
+
placeholder: '',
|
|
41032
|
+
errorKeys: {
|
|
41033
|
+
required: 'codeRequired',
|
|
41034
|
+
minlength: 'codeMinLength',
|
|
41035
|
+
},
|
|
41036
|
+
validators: [Validators.required, Validators.minLength(6)],
|
|
41037
|
+
order: 0,
|
|
41038
|
+
state: ComponentStates.ENABLED,
|
|
41039
|
+
length: 6,
|
|
41040
|
+
allowNumbersOnly: true,
|
|
41041
|
+
autoFocus: true,
|
|
41042
|
+
},
|
|
41043
|
+
],
|
|
41044
|
+
},
|
|
41045
|
+
],
|
|
41046
|
+
actions: {
|
|
41047
|
+
...button({ text: '', type: 'submit', fill: 'solid', expand: 'block' }),
|
|
41048
|
+
token: 'mfa-reset-confirm-submit',
|
|
41049
|
+
textKey: 'mfaResetConfirmSubmit',
|
|
41050
|
+
},
|
|
41051
|
+
state: this._mfaResetFormState(),
|
|
41052
|
+
}));
|
|
41053
|
+
// ==========================================
|
|
40899
41054
|
// FORGOT PASSWORD FORM (computed)
|
|
40900
41055
|
// ==========================================
|
|
40901
41056
|
this.forgotPasswordFormProps = computed(() => this.i18nHelper.resolveForm({
|
|
@@ -41026,6 +41181,7 @@ class LoginComponent {
|
|
|
41026
41181
|
next: () => {
|
|
41027
41182
|
this._loginFormState.set(ComponentStates.ENABLED);
|
|
41028
41183
|
if (this.authService.mfaPending().required) {
|
|
41184
|
+
this._mfaResetEmail.set(email); // B: prefill del reset por email
|
|
41029
41185
|
this.openMFAVerifyModal();
|
|
41030
41186
|
return;
|
|
41031
41187
|
}
|
|
@@ -41154,6 +41310,7 @@ class LoginComponent {
|
|
|
41154
41310
|
openMFAVerifyModal() {
|
|
41155
41311
|
const method = this.authService.mfaPending().method;
|
|
41156
41312
|
this._mfaMethod.set(method);
|
|
41313
|
+
this.mfaUseBackupCode.set(false);
|
|
41157
41314
|
this._mfaVerifyFormState.set(ComponentStates.ENABLED);
|
|
41158
41315
|
this.isMFAVerifyModalOpen = true;
|
|
41159
41316
|
this.onMFARequired.emit({ method });
|
|
@@ -41161,6 +41318,60 @@ class LoginComponent {
|
|
|
41161
41318
|
closeMFAVerifyModal() {
|
|
41162
41319
|
this.isMFAVerifyModalOpen = false;
|
|
41163
41320
|
}
|
|
41321
|
+
/** A: alterna entre el código TOTP (6 díg) y un código de respaldo (8 chars). */
|
|
41322
|
+
toggleMFABackupCode() {
|
|
41323
|
+
this.mfaUseBackupCode.update(v => !v);
|
|
41324
|
+
}
|
|
41325
|
+
// ==========================================
|
|
41326
|
+
// MFA RESET HANDLERS — B: recovery por email
|
|
41327
|
+
// ==========================================
|
|
41328
|
+
openMFAResetModal() {
|
|
41329
|
+
this._mfaResetStep.set('request');
|
|
41330
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41331
|
+
this.isMFAResetModalOpen = true;
|
|
41332
|
+
}
|
|
41333
|
+
closeMFAResetModal() {
|
|
41334
|
+
this.isMFAResetModalOpen = false;
|
|
41335
|
+
this._mfaResetStep.set('request');
|
|
41336
|
+
}
|
|
41337
|
+
mfaResetRequestHandler(event) {
|
|
41338
|
+
const email = event.fields['email'];
|
|
41339
|
+
if (!email) {
|
|
41340
|
+
this.showToast(this.t('enterEmail'));
|
|
41341
|
+
return;
|
|
41342
|
+
}
|
|
41343
|
+
this._mfaResetFormState.set(ComponentStates.WORKING);
|
|
41344
|
+
this._mfaResetEmail.set(email);
|
|
41345
|
+
this.authService.requestMFAReset({ email }).subscribe({
|
|
41346
|
+
next: () => {
|
|
41347
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41348
|
+
this._mfaResetStep.set('confirm');
|
|
41349
|
+
this.showToast(this.t('mfaResetCodeSent'));
|
|
41350
|
+
},
|
|
41351
|
+
error: err => {
|
|
41352
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41353
|
+
this.handleError(err, 'mfa');
|
|
41354
|
+
},
|
|
41355
|
+
});
|
|
41356
|
+
}
|
|
41357
|
+
mfaResetConfirmHandler(event) {
|
|
41358
|
+
const code = event.fields['code'];
|
|
41359
|
+
this._mfaResetFormState.set(ComponentStates.WORKING);
|
|
41360
|
+
this.authService.confirmMFAReset({ email: this._mfaResetEmail(), code }).subscribe({
|
|
41361
|
+
next: () => {
|
|
41362
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41363
|
+
// MFA deshabilitado → cerramos ambos modales; el user reinicia el login
|
|
41364
|
+
// (ahora sin 2º factor) y re-enrola TOTP desde ajustes.
|
|
41365
|
+
this.closeMFAResetModal();
|
|
41366
|
+
this.closeMFAVerifyModal();
|
|
41367
|
+
this.showToast(this.t('mfaResetDone'));
|
|
41368
|
+
},
|
|
41369
|
+
error: err => {
|
|
41370
|
+
this._mfaResetFormState.set(ComponentStates.ENABLED);
|
|
41371
|
+
this.handleError(err, 'mfa');
|
|
41372
|
+
},
|
|
41373
|
+
});
|
|
41374
|
+
}
|
|
41164
41375
|
verifyMFAHandler(event) {
|
|
41165
41376
|
const code = event.fields['code'];
|
|
41166
41377
|
this._mfaVerifyFormState.set(ComponentStates.WORKING);
|
|
@@ -41377,7 +41588,7 @@ class LoginComponent {
|
|
|
41377
41588
|
this.stopResetResendCooldown();
|
|
41378
41589
|
}
|
|
41379
41590
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
41380
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LoginComponent, isStandalone: true, selector: "val-login", inputs: { props: "props" }, outputs: { onSuccess: "onSuccess", onError: "onError", onMFARequired: "onMFARequired" }, ngImport: i0, template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { 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: ImageComponent, selector: "val-image", inputs: ["props"] }] }); }
|
|
41591
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LoginComponent, isStandalone: true, selector: "val-login", inputs: { props: "props" }, outputs: { onSuccess: "onSuccess", onError: "onError", onMFARequired: "onMFARequired" }, ngImport: i0, template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n @if (mfaMethod() === 'TOTP') {\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n @if (mfaUseBackupCode()) {\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseTotpAction') }}</a>\n } @else { {{ t('mfaUseBackupLink') }}\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseBackupAction') }}</a>\n }\n </ion-text>\n </div>\n }\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('mfaLostBackupLink') }}\n <a (click)=\"openMFAResetModal()\">{{ t('mfaResetAction') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Reset Modal (B) \u2014 recovery por email cuando no hay backup codes -->\n<ion-modal [isOpen]=\"isMFAResetModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"closeMFAResetModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAResetModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n @if (mfaResetStep() === 'request') {\n <val-form [props]=\"mfaResetRequestFormProps()\" (onSubmit)=\"mfaResetRequestHandler($event)\" />\n } @else {\n <val-form [props]=\"mfaResetConfirmFormProps()\" (onSubmit)=\"mfaResetConfirmHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { 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: ImageComponent, selector: "val-image", inputs: ["props"] }] }); }
|
|
41381
41592
|
}
|
|
41382
41593
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, decorators: [{
|
|
41383
41594
|
type: Component,
|
|
@@ -41393,7 +41604,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
41393
41604
|
IonToolbar,
|
|
41394
41605
|
FormComponent,
|
|
41395
41606
|
ImageComponent,
|
|
41396
|
-
], template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"] }]
|
|
41607
|
+
], template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n @if (mfaMethod() === 'TOTP') {\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n @if (mfaUseBackupCode()) {\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseTotpAction') }}</a>\n } @else { {{ t('mfaUseBackupLink') }}\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseBackupAction') }}</a>\n }\n </ion-text>\n </div>\n }\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('mfaLostBackupLink') }}\n <a (click)=\"openMFAResetModal()\">{{ t('mfaResetAction') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Reset Modal (B) \u2014 recovery por email cuando no hay backup codes -->\n<ion-modal [isOpen]=\"isMFAResetModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"closeMFAResetModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAResetModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n @if (mfaResetStep() === 'request') {\n <val-form [props]=\"mfaResetRequestFormProps()\" (onSubmit)=\"mfaResetRequestHandler($event)\" />\n } @else {\n <val-form [props]=\"mfaResetConfirmFormProps()\" (onSubmit)=\"mfaResetConfirmHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"] }]
|
|
41397
41608
|
}], propDecorators: { props: [{
|
|
41398
41609
|
type: Input
|
|
41399
41610
|
}], onSuccess: [{
|
|
@@ -52178,7 +52389,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52178
52389
|
inviteSuccess: 'Invitación enviada.',
|
|
52179
52390
|
inviteError: 'No se pudo enviar la invitación.',
|
|
52180
52391
|
membersError: 'No se pudieron cargar los miembros.',
|
|
52181
|
-
leaveTitle: 'Salir de la organización',
|
|
52392
|
+
leaveTitle: '⚠️ Salir de la organización',
|
|
52182
52393
|
leaveHint: 'Dejarás de tener acceso a esta organización.',
|
|
52183
52394
|
leaveCta: 'Salir',
|
|
52184
52395
|
leaveOwnerHint: 'Eres el propietario. Transfiere la propiedad antes de salir.',
|
|
@@ -52194,7 +52405,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52194
52405
|
retry: 'Reintentar',
|
|
52195
52406
|
saveError: 'No se pudo guardar. Intenta de nuevo.',
|
|
52196
52407
|
leaveError: 'No se pudo salir. Intenta de nuevo.',
|
|
52197
|
-
transferTitle: 'Transferir propiedad',
|
|
52408
|
+
transferTitle: '⚠️ Transferir propiedad',
|
|
52198
52409
|
transferHint: 'Elige un miembro para transferirle la propiedad de la organización.',
|
|
52199
52410
|
transferSelect: 'Seleccionar nuevo propietario',
|
|
52200
52411
|
transferCta: 'Transferir',
|
|
@@ -52245,7 +52456,10 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52245
52456
|
permDelete: 'Eliminar',
|
|
52246
52457
|
permManage: 'Gestionar',
|
|
52247
52458
|
membersShowMore: 'Ver más',
|
|
52248
|
-
|
|
52459
|
+
orgsNewQuestion: '¿Necesitas otra organización?',
|
|
52460
|
+
orgsNewHint: 'Crea un espacio separado para otro equipo, empresa o proyecto.',
|
|
52461
|
+
orgsNewCta: 'Nueva organización',
|
|
52462
|
+
deleteTitle: '⚠️ Eliminar organización',
|
|
52249
52463
|
deleteHint: 'Esta acción es permanente e irreversible. Todos los miembros perderán el acceso.',
|
|
52250
52464
|
deleteCta: 'Eliminar organización',
|
|
52251
52465
|
deleteConfirm: '¿Estás seguro? Esta acción eliminará permanentemente la organización y no se puede deshacer.',
|
|
@@ -52289,7 +52503,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52289
52503
|
retry: 'Retry',
|
|
52290
52504
|
saveError: 'Could not save. Please try again.',
|
|
52291
52505
|
leaveError: 'Could not leave. Please try again.',
|
|
52292
|
-
transferTitle: 'Transfer ownership',
|
|
52506
|
+
transferTitle: '⚠️ Transfer ownership',
|
|
52293
52507
|
transferHint: 'Choose a member to transfer ownership of this organization.',
|
|
52294
52508
|
transferSelect: 'Select new owner',
|
|
52295
52509
|
transferCta: 'Transfer',
|
|
@@ -52340,7 +52554,10 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
52340
52554
|
permDelete: 'Delete',
|
|
52341
52555
|
permManage: 'Manage',
|
|
52342
52556
|
membersShowMore: 'Show more',
|
|
52343
|
-
|
|
52557
|
+
orgsNewQuestion: 'Need another organization?',
|
|
52558
|
+
orgsNewHint: 'Create a separate space for another team, company or project.',
|
|
52559
|
+
orgsNewCta: 'New organization',
|
|
52560
|
+
deleteTitle: '⚠️ Delete organization',
|
|
52344
52561
|
deleteHint: 'This action is permanent and irreversible. All members will lose access.',
|
|
52345
52562
|
deleteCta: 'Delete organization',
|
|
52346
52563
|
deleteConfirm: 'Are you sure? This action will permanently delete the organization and cannot be undone.',
|
|
@@ -52406,6 +52623,7 @@ class OrganizationViewComponent {
|
|
|
52406
52623
|
showMembers: merged.showMembers ?? true,
|
|
52407
52624
|
showRoles: merged.showRoles ?? true,
|
|
52408
52625
|
showInvite: merged.showInvite ?? true,
|
|
52626
|
+
showNewOrgCta: merged.showNewOrgCta ?? false,
|
|
52409
52627
|
showTransferOwnership: merged.showTransferOwnership ?? true,
|
|
52410
52628
|
showLeave: merged.showLeave ?? true,
|
|
52411
52629
|
showDeleteOrg: merged.showDeleteOrg ?? true,
|
|
@@ -52418,6 +52636,7 @@ class OrganizationViewComponent {
|
|
|
52418
52636
|
onOwnershipTransferred: merged.onOwnershipTransferred,
|
|
52419
52637
|
onLeftOrg: merged.onLeftOrg,
|
|
52420
52638
|
onOrgDeleted: merged.onOrgDeleted,
|
|
52639
|
+
onOrgCreated: merged.onOrgCreated,
|
|
52421
52640
|
};
|
|
52422
52641
|
});
|
|
52423
52642
|
this.org = signal(null);
|
|
@@ -52426,6 +52645,7 @@ class OrganizationViewComponent {
|
|
|
52426
52645
|
this.loading = signal(false);
|
|
52427
52646
|
this.leaving = signal(false);
|
|
52428
52647
|
this.permissionsOpen = signal(false);
|
|
52648
|
+
this.createOrgModalOpen = signal(false);
|
|
52429
52649
|
this.orgLoadError = signal(null);
|
|
52430
52650
|
this.orgErrorState = computed(() => {
|
|
52431
52651
|
this.i18n.lang();
|
|
@@ -52571,6 +52791,28 @@ class OrganizationViewComponent {
|
|
|
52571
52791
|
type: 'button',
|
|
52572
52792
|
state: this.deleting() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
|
|
52573
52793
|
}));
|
|
52794
|
+
this.newOrgCtaProps = computed(() => ({
|
|
52795
|
+
title: this.tt('orgsNewQuestion'),
|
|
52796
|
+
description: this.tt('orgsNewHint'),
|
|
52797
|
+
padding: '16px',
|
|
52798
|
+
borderRadius: '14px',
|
|
52799
|
+
actions: {
|
|
52800
|
+
position: 'right',
|
|
52801
|
+
columned: false,
|
|
52802
|
+
buttons: [
|
|
52803
|
+
{
|
|
52804
|
+
text: this.tt('orgsNewCta'),
|
|
52805
|
+
color: 'primary',
|
|
52806
|
+
fill: 'solid',
|
|
52807
|
+
shape: 'round',
|
|
52808
|
+
size: 'default',
|
|
52809
|
+
type: 'button',
|
|
52810
|
+
state: 'ENABLED',
|
|
52811
|
+
token: 'org-new',
|
|
52812
|
+
},
|
|
52813
|
+
],
|
|
52814
|
+
},
|
|
52815
|
+
}));
|
|
52574
52816
|
/** Guarda para detectar el primer disparo del effect (evita doble carga). */
|
|
52575
52817
|
this.lastLoadedOrgId = null;
|
|
52576
52818
|
const ns = this.ns;
|
|
@@ -52680,6 +52922,13 @@ class OrganizationViewComponent {
|
|
|
52680
52922
|
this.deleting.set(false);
|
|
52681
52923
|
}
|
|
52682
52924
|
}
|
|
52925
|
+
onNewOrg() {
|
|
52926
|
+
this.createOrgModalOpen.set(true);
|
|
52927
|
+
}
|
|
52928
|
+
onOrgCreated(newOrg) {
|
|
52929
|
+
this.createOrgModalOpen.set(false);
|
|
52930
|
+
this.resolvedConfig().onOrgCreated?.(newOrg);
|
|
52931
|
+
}
|
|
52683
52932
|
onOpenInvite() {
|
|
52684
52933
|
void this.modalService.openAdaptive({
|
|
52685
52934
|
component: InviteMemberModalComponent,
|
|
@@ -53124,6 +53373,13 @@ class OrganizationViewComponent {
|
|
|
53124
53373
|
</section>
|
|
53125
53374
|
}
|
|
53126
53375
|
|
|
53376
|
+
@if (resolvedConfig().showNewOrgCta) {
|
|
53377
|
+
<!-- Nueva org CTA -->
|
|
53378
|
+
<section class="settings-section">
|
|
53379
|
+
<val-cta-card [props]="newOrgCtaProps()" (onAction)="onNewOrg()" />
|
|
53380
|
+
</section>
|
|
53381
|
+
}
|
|
53382
|
+
|
|
53127
53383
|
@if (resolvedConfig().showInvite) {
|
|
53128
53384
|
<!-- Invite: solo admin+ (gating RBAC interno) -->
|
|
53129
53385
|
<section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
|
|
@@ -53272,12 +53528,19 @@ class OrganizationViewComponent {
|
|
|
53272
53528
|
[config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
|
|
53273
53529
|
(dismissed)="permissionsOpen.set(false)"
|
|
53274
53530
|
/>
|
|
53531
|
+
<val-create-org-modal
|
|
53532
|
+
[isOpen]="createOrgModalOpen()"
|
|
53533
|
+
(dismissed)="createOrgModalOpen.set(false)"
|
|
53534
|
+
(created)="onOrgCreated($event)"
|
|
53535
|
+
/>
|
|
53275
53536
|
</div>
|
|
53276
|
-
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }] }); }
|
|
53537
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }] }); }
|
|
53277
53538
|
}
|
|
53278
53539
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
|
|
53279
53540
|
type: Component,
|
|
53280
53541
|
args: [{ selector: 'val-organization-view', standalone: true, imports: [
|
|
53542
|
+
CreateOrgModalComponent,
|
|
53543
|
+
CtaCardComponent,
|
|
53281
53544
|
DisplayComponent,
|
|
53282
53545
|
EmptyStateComponent,
|
|
53283
53546
|
HasPermissionDirective,
|
|
@@ -53448,6 +53711,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
53448
53711
|
</section>
|
|
53449
53712
|
}
|
|
53450
53713
|
|
|
53714
|
+
@if (resolvedConfig().showNewOrgCta) {
|
|
53715
|
+
<!-- Nueva org CTA -->
|
|
53716
|
+
<section class="settings-section">
|
|
53717
|
+
<val-cta-card [props]="newOrgCtaProps()" (onAction)="onNewOrg()" />
|
|
53718
|
+
</section>
|
|
53719
|
+
}
|
|
53720
|
+
|
|
53451
53721
|
@if (resolvedConfig().showInvite) {
|
|
53452
53722
|
<!-- Invite: solo admin+ (gating RBAC interno) -->
|
|
53453
53723
|
<section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
|
|
@@ -53596,6 +53866,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
53596
53866
|
[config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
|
|
53597
53867
|
(dismissed)="permissionsOpen.set(false)"
|
|
53598
53868
|
/>
|
|
53869
|
+
<val-create-org-modal
|
|
53870
|
+
[isOpen]="createOrgModalOpen()"
|
|
53871
|
+
(dismissed)="createOrgModalOpen.set(false)"
|
|
53872
|
+
(created)="onOrgCreated($event)"
|
|
53873
|
+
/>
|
|
53599
53874
|
</div>
|
|
53600
53875
|
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"] }]
|
|
53601
53876
|
}], ctorParameters: () => [], propDecorators: { config: [{
|