diff --git a/src/app/access-control/access-control.component.html b/src/app/access-control/access-control.component.html index 191b3e53e..c65d59420 100644 --- a/src/app/access-control/access-control.component.html +++ b/src/app/access-control/access-control.component.html @@ -203,7 +203,7 @@ } @else { Manage devices arrow_forward } - Type · TOKEN + Type · DSC @@ -396,17 +396,28 @@ } @else { -
- check_circle +
+ {{ sms.mfaEnabled ? 'check_circle' : 'pause_circle' }}
- SMS OTP active - OTP will be sent to +91 {{ sms.destination }} on every sign-in. + {{ sms.mfaEnabled ? 'SMS OTP active' : 'SMS OTP verified · 2FA disabled' }} + + Number on file: +91 {{ sms.destination }} + @if (sms.mfaEnabled) { + — OTP will be sent on every sign-in. + } @else { + — re-enable any time without re-verifying. + } +
- + @if (sms.mfaEnabled) { + + } @else { + + }
}
@@ -566,17 +577,28 @@ } @else { -
- check_circle +
+ {{ email.mfaEnabled ? 'check_circle' : 'pause_circle' }}
- Email OTP active - OTP will be sent to {{ email.destination }} on every sign-in. + {{ email.mfaEnabled ? 'Email OTP active' : 'Email OTP verified · 2FA disabled' }} + + Address on file: {{ email.destination }} + @if (email.mfaEnabled) { + — OTP will be sent on every sign-in. + } @else { + — re-enable any time without re-verifying. + } +
- + @if (email.mfaEnabled) { + + } @else { + + }
}
@@ -697,7 +719,7 @@
memory

Hardware Token

- Type · TOKEN + Type · DSC
Map DSC + } @else if (currentHwItem.mfaEnabled) { + + } @else { } } @else { diff --git a/src/app/access-control/access-control.component.scss b/src/app/access-control/access-control.component.scss index d2abc00a2..2b9e1e9e6 100644 --- a/src/app/access-control/access-control.component.scss +++ b/src/app/access-control/access-control.component.scss @@ -959,6 +959,21 @@ font-size: 11.5px; } } + + /* Verified but 2FA disabled — muted amber to read as "paused", not "off". */ + &.is-disabled { + background: #fffbeb; + border-color: #fde68a; + color: #78350f; + + > mat-icon { + color: #d97706; + } + + > div code { + border-color: #fde68a; + } + } } .ac-ff { diff --git a/src/app/access-control/access-control.component.ts b/src/app/access-control/access-control.component.ts index 2964b098e..8c36fcb7a 100644 --- a/src/app/access-control/access-control.component.ts +++ b/src/app/access-control/access-control.component.ts @@ -23,6 +23,9 @@ import { UsersService } from 'app/users/users.service'; import { AccessControlService, MfaFactor } from './access-control.service'; import { DscService } from 'app/users/dsc/dsc.service'; import { EnrollDscDialogComponent } from 'app/users/dsc/enroll-dsc-dialog/enroll-dsc-dialog.component'; +import { RevokeDscDialogComponent } from 'app/users/dsc/revoke-dsc-dialog/revoke-dsc-dialog.component'; +import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component'; +import { ErrorDialogComponent } from 'app/shared/error-dialog/error-dialog.component'; type HwSubKind = 'smart-card' | 'digital-signature' | 'biometric'; type MethodKey = 'sms' | 'email' | 'totp' | 'hardware'; @@ -35,14 +38,26 @@ interface OtpChannelState { otp: string; otpSent: boolean; resendIn: number; + /** + * True once the destination has passed an OTP challenge at least once. + * This is a permanent attestation of the destination — disabling 2FA + * does NOT reset this. To clear, the user must explicitly "Change". + */ verified: boolean; + /** + * True when the channel is currently enforcing 2FA at sign-in. + * Independent of `verified`: a destination can be verified but + * temporarily disabled (factor row has disabled_on set), and later + * re-enabled without going through OTP again. + */ + mfaEnabled: boolean; errorMsg: string; busy: boolean; /** otpId returned from the OTP-generate endpoint, used by verify */ otpId: number | null; /** verifiedOn timestamp from the server (ISO string) */ verifiedOn: string; - /** Persisted factor id from m_user_mfa_factor */ + /** Persisted factor id from m_user_mfa_factor (null when no active factor row). */ factorId: number | null; } @@ -67,6 +82,10 @@ interface HardwareItem { busy: boolean; /** DSC-only: whether the mapped DSC is currently enabled as a 2FA factor. */ mfaEnabled?: boolean; + /** DSC-only: persisted MFA factor id when enabled (used for disable). */ + factorId?: number | null; + /** DSC-only: id of the m_user_dsc row (needed for revoke). */ + dscId?: number | null; } interface PersistedPrefs { @@ -294,12 +313,22 @@ export class AccessControlComponent implements OnInit, OnDestroy { } else if (f.factorType === 'TOTP' && f.verified) { this.totp.secret = f.secret || this.totp.secret; this.totp.verified = true; - } else if (f.factorType === 'TOKEN') { - const item = this.hardware.find((h) => h.serial === f.serial); + } else if (f.factorType === 'DSC') { + // Match by serial, falling back to the DSC tile if no serial match + // (DSC is the only hardware kind backed by the factor API today). + // A factor with disabled_on set is NOT currently enforcing 2FA — + // mapping (m_user_dsc cert is ACTIVE) and 2FA-enabled (factor row + // exists with disabled_on IS NULL) are two independent states. + const item = + this.hardware.find((h) => h.serial === f.serial) || + this.hardware.find((h) => h.kind === 'digital-signature'); if (item) { item.registered = true; item.serial = f.serial || item.serial; item.registeredAt = f.verifiedOn || item.registeredAt; + const isActive = !f.disabledOn && f.verified !== false; + item.mfaEnabled = isActive; + item.factorId = isActive ? (f.id ?? null) : null; } } } @@ -319,10 +348,12 @@ export class AccessControlComponent implements OnInit, OnDestroy { /** Wipe every factor's verified / registered flags before re-applying API truth. */ private resetAllFactors(): void { this.sms.verified = false; + this.sms.mfaEnabled = false; this.sms.destinationConfirmed = false; this.sms.factorId = null; this.sms.verifiedOn = ''; this.email.verified = false; + this.email.mfaEnabled = false; this.email.destinationConfirmed = false; this.email.factorId = null; this.email.verifiedOn = ''; @@ -332,14 +363,22 @@ export class AccessControlComponent implements OnInit, OnDestroy { h.registered = false; h.serial = ''; h.registeredAt = ''; + h.mfaEnabled = false; + h.factorId = null; + h.dscId = null; } } private applyFactor(channel: OtpChannelState, f: MfaFactor): void { if (f.destination) channel.destination = f.destination; + // `verified` is the permanent attestation that this destination passed + // OTP at least once. `mfaEnabled` is the live 2FA toggle — false when + // the factor row has disabled_on set. channel.verified = !!f.verified; channel.destinationConfirmed = !!f.verified; - channel.factorId = f.id ?? null; + const isActive = !f.disabledOn && !!f.verified; + channel.mfaEnabled = isActive; + channel.factorId = isActive ? (f.id ?? null) : null; channel.verifiedOn = f.verifiedOn || ''; } @@ -434,6 +473,10 @@ export class AccessControlComponent implements OnInit, OnDestroy { const finishVerified = (saved?: MfaFactor) => { channel.busy = false; channel.verified = true; + // Initial verification auto-enables the factor for 2FA — that's + // the user's intent when they walked through the OTP setup flow. + // Subsequent disable/enable is independent of `verified`. + channel.mfaEnabled = true; channel.otpSent = false; channel.resendIn = 0; channel.factorId = saved?.id ?? null; @@ -473,6 +516,41 @@ export class AccessControlComponent implements OnInit, OnDestroy { channel.otpId = null; } + /** + * Re-enable a SMS/Email channel whose destination was previously verified + * but is currently disabled (factor row has disabled_on set). This skips + * OTP re-verification — the attestation persists via `verified=true`. + * Saves a fresh factor row server-side and adopts its new id. + */ + enableOtpChannel(channel: OtpChannelState): void { + if (!channel.verified || !channel.destination) { + this.showError('Verify your destination with an OTP before enabling 2FA.'); + return; + } + const label = channel.typeCode === 'SMS' ? 'SMS OTP' : 'Email OTP'; + const finish = (saved?: MfaFactor) => { + channel.busy = false; + channel.mfaEnabled = true; + channel.factorId = saved?.id ?? channel.factorId ?? null; + this.savePrefs(); + this.showSuccess(`${label} enabled for 2FA`, `${label} will be required at sign-in for ${this.username}.`); + }; + if (!this.mfaFactorsApiAvailable) { + finish(); + return; + } + channel.busy = true; + this.accessControlService.saveFactor({ factorType: channel.typeCode, destination: channel.destination }).subscribe({ + next: (saved) => finish(saved), + error: (err) => { + channel.busy = false; + this.showError( + err?.error?.defaultUserMessage || err?.error?.message || `Could not enable ${label} as a 2FA factor.` + ); + } + }); + } + disableOtpChannel(channel: OtpChannelState): void { const label = channel.typeCode === 'SMS' ? 'SMS OTP' : 'Email OTP'; const destination = @@ -490,19 +568,17 @@ export class AccessControlComponent implements OnInit, OnDestroy { .afterClosed() .subscribe((response: any) => { if (!response?.confirmed) return; + // Disable only flips the 2FA toggle. The destination + verified + // attestation stay so the user can re-enable later without + // re-running OTP. To wipe destination, the user must use "Change". const finish = () => { - channel.verified = false; - channel.otp = ''; - channel.otpSent = false; - channel.destinationConfirmed = false; - channel.errorMsg = ''; - channel.resendIn = 0; - channel.otpId = null; + channel.mfaEnabled = false; channel.factorId = null; - channel.verifiedOn = ''; + channel.busy = false; this.savePrefs(); }; if (this.mfaFactorsApiAvailable && channel.factorId != null) { + channel.busy = true; this.accessControlService.disableFactor(channel.factorId).subscribe({ next: () => finish(), error: () => finish() @@ -646,6 +722,7 @@ export class AccessControlComponent implements OnInit, OnDestroy { if (!result?.enrolled) return; const dsc = result.dsc; item.registered = true; + item.dscId = dsc?.id ?? null; item.serial = dsc?.dscSerialNoFull || dsc?.dscSerialNoMasked || ''; item.registeredAt = new Date().toLocaleString(); item.mfaEnabled = false; @@ -654,13 +731,137 @@ export class AccessControlComponent implements OnInit, OnDestroy { } enableDscMfa(item: HardwareItem): void { - item.mfaEnabled = true; - this.savePrefs(); + if (!item.registered || !item.serial) { + this.showError('Map a DSC before enabling it as a 2FA factor.'); + return; + } + const finish = (saved?: MfaFactor) => { + item.busy = false; + item.mfaEnabled = true; + item.factorId = saved?.id ?? item.factorId ?? null; + this.savePrefs(); + this.showSuccess( + 'DSC enabled for 2FA', + `Sign-in for ${this.username} will now require this Digital Signature token.` + ); + }; + if (!this.mfaFactorsApiAvailable) { + finish(); + return; + } + item.busy = true; + this.accessControlService.saveFactor({ factorType: 'DSC', serial: item.serial }).subscribe({ + next: (saved) => finish(saved), + error: (err) => { + item.busy = false; + this.showError( + err?.error?.defaultUserMessage || err?.error?.message || 'Could not enable DSC as a 2FA factor.' + ); + } + }); } disableDscMfa(item: HardwareItem): void { - item.mfaEnabled = false; - this.savePrefs(); + this.dialog + .open(DisableConfirmDialogComponent, { + width: '440px', + autoFocus: false, + data: { + title: 'Disable DSC for 2FA', + message: + 'The Digital Signature token will no longer be required at sign-in. The mapping itself stays in place — you can re-enable it any time.', + detail: item.serial ? `Currently active for serial ${item.serial}` : '' + } + }) + .afterClosed() + .subscribe((response: any) => { + if (!response?.confirmed) return; + const finish = () => { + item.busy = false; + item.mfaEnabled = false; + item.factorId = null; + this.savePrefs(); + this.showSuccess( + 'DSC disabled for 2FA', + 'The token is no longer used as a second factor. Sign-in will fall back to your password only.' + ); + }; + if (this.mfaFactorsApiAvailable && item.factorId != null) { + item.busy = true; + this.accessControlService.disableFactor(item.factorId).subscribe({ + next: () => finish(), + error: () => finish() // keep UX consistent — local state already reflects the user's intent + }); + } else { + finish(); + } + }); + } + + /** + * Revoke a mapped DSC. Opens the existing reason-capture dialog (which calls + * /v1/dsc/{id} DELETE itself), then on success disables the MFA factor if + * it was enabled and clears the tile back to the unmapped state. Mapping + * and 2FA-enabled are independent — both must be cleared on revoke. + */ + revokeDsc(item: HardwareItem): void { + if (!item.dscId) { + this.showError('Cannot revoke — DSC certificate id is missing. Reload the page and try again.'); + return; + } + // The dialog only needs id + a serial to display; build a minimal stub + // rather than fetching the full BankUserDsc just to surface a confirm. + const dscStub = { + id: item.dscId, + dscSerialNoMasked: item.serial, + dscSerialNoFull: item.serial + } as any; + const ref = this.dialog.open(RevokeDscDialogComponent, { + width: '460px', + autoFocus: false, + disableClose: true, + data: { dsc: dscStub } + }); + ref.afterClosed().subscribe((result: any) => { + if (!result?.revoked) return; + const clearLocal = () => { + item.busy = false; + item.registered = false; + item.serial = ''; + item.registeredAt = ''; + item.mfaEnabled = false; + item.factorId = null; + item.dscId = null; + this.selectedHwKind = null; + this.savePrefs(); + this.showSuccess( + 'DSC revoked', + 'The certificate has been revoked and unbound from your account. You can map a new DSC any time.' + ); + }; + // If 2FA was enabled, also disable the factor row so the next sign-in + // doesn't try to enforce a now-revoked certificate. + if (this.mfaFactorsApiAvailable && item.factorId != null) { + item.busy = true; + this.accessControlService.disableFactor(item.factorId).subscribe({ + next: () => clearLocal(), + error: () => clearLocal() // revoke succeeded; factor cleanup is best-effort + }); + } else { + clearLocal(); + } + }); + } + + private showSuccess(title: string, message: string): void { + this.dialog.open(SuccessDialogComponent, { + data: { title, message, buttonLabel: 'OK' }, + panelClass: 'sd-panel' + }); + } + + private showError(message: string): void { + this.dialog.open(ErrorDialogComponent, { data: message }); } /** @@ -676,6 +877,7 @@ export class AccessControlComponent implements OnInit, OnDestroy { const active = (dscs || []).find((d) => (d?.certStatus || '').toUpperCase() === 'ACTIVE'); if (active) { dscItem.registered = true; + dscItem.dscId = active.id; dscItem.serial = active.dscSerialNoMasked || active.dscSerialNoFull || dscItem.serial; dscItem.registeredAt = dscItem.registeredAt || new Date().toLocaleString(); } @@ -695,17 +897,31 @@ export class AccessControlComponent implements OnInit, OnDestroy { // ───────────────────────────── Aggregate state - /** True when at least one method (any type) is verified/registered */ + /** + * True when at least one method is currently enforcing 2FA. For SMS/Email + * and Hardware, "active" requires the live 2FA toggle (`mfaEnabled`) — a + * verified-but-disabled destination does NOT count. TOTP is single-state. + */ get anyMethodActive(): boolean { - return this.sms.verified || this.email.verified || this.totp.verified || this.hardware.some((h) => h.registered); + return ( + (this.sms.verified && this.sms.mfaEnabled) || + (this.email.verified && this.email.mfaEnabled) || + this.totp.verified || + this.hardware.some((h) => h.registered && h.mfaEnabled === true) + ); } - /** Returns the single currently-active MFA method, or null if none. */ + /** + * Returns the single currently-active MFA method, or null if none. + * "Active" means the method is currently enforcing 2FA at sign-in. A + * verified-but-disabled SMS/Email channel, or a mapped-but-not-enabled + * DSC, does NOT lock out other methods. + */ get activeMethod(): MethodKey | null { - if (this.sms.verified) return 'sms'; - if (this.email.verified) return 'email'; + if (this.sms.verified && this.sms.mfaEnabled) return 'sms'; + if (this.email.verified && this.email.mfaEnabled) return 'email'; if (this.totp.verified) return 'totp'; - if (this.hardware.some((h) => h.registered)) return 'hardware'; + if (this.hardware.some((h) => h.registered && h.mfaEnabled === true)) return 'hardware'; return null; } @@ -803,6 +1019,7 @@ export class AccessControlComponent implements OnInit, OnDestroy { otpSent: false, resendIn: 0, verified: false, + mfaEnabled: false, errorMsg: '', busy: false, otpId: null, diff --git a/src/app/access-control/access-control.service.ts b/src/app/access-control/access-control.service.ts index 2363964c8..10c73be32 100644 --- a/src/app/access-control/access-control.service.ts +++ b/src/app/access-control/access-control.service.ts @@ -22,7 +22,7 @@ export interface OtpVerifyResponse { message?: string; } -export type MfaFactorType = 'SMS' | 'EMAIL' | 'TOTP' | 'TOKEN'; +export type MfaFactorType = 'SMS' | 'EMAIL' | 'TOTP' | 'TOKEN' | 'DSC'; export interface MfaFactor { id?: number; @@ -32,6 +32,8 @@ export interface MfaFactor { serial?: string; verified: boolean; verifiedOn?: string; + /** ISO timestamp when the factor was logically disabled — when set, the factor is NOT active. */ + disabledOn?: string; otpLogId?: number; } diff --git a/src/app/centers/centers-view/notes-tab/notes-tab.component.ts b/src/app/centers/centers-view/notes-tab/notes-tab.component.ts index f3b7c1934..f8f6646c9 100644 --- a/src/app/centers/centers-view/notes-tab/notes-tab.component.ts +++ b/src/app/centers/centers-view/notes-tab/notes-tab.component.ts @@ -15,7 +15,7 @@ import { EntityNotesTabComponent } from '../../../shared/tabs/entity-notes-tab/e import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ - selector: 'mifosx-notes-tab', + selector: 'mifosx-centers-notes-tab', templateUrl: './notes-tab.component.html', styleUrls: ['./notes-tab.component.scss'], imports: [ diff --git a/src/app/clients/clients-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts b/src/app/clients/clients-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts index ff7f33a25..d7e91894f 100644 --- a/src/app/clients/clients-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts +++ b/src/app/clients/clients-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts @@ -23,7 +23,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; * Unassign staff dialog component. */ @Component({ - selector: 'mifosx-unassign-staff-dialog', + selector: 'mifosx-clients-unassign-staff-dialog', templateUrl: './unassign-staff-dialog.component.html', styleUrls: ['./unassign-staff-dialog.component.scss'], imports: [ diff --git a/src/app/clients/clients-view/notes-tab/notes-tab.component.ts b/src/app/clients/clients-view/notes-tab/notes-tab.component.ts index 682d51bbd..de896b571 100644 --- a/src/app/clients/clients-view/notes-tab/notes-tab.component.ts +++ b/src/app/clients/clients-view/notes-tab/notes-tab.component.ts @@ -22,7 +22,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; * Notes Tab Component */ @Component({ - selector: 'mifosx-notes-tab', + selector: 'mifosx-clients-notes-tab', templateUrl: './notes-tab.component.html', styleUrls: ['./notes-tab.component.scss'], imports: [ diff --git a/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.ts b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.ts index 1493f27d2..a8b52dc80 100644 --- a/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.ts +++ b/src/app/current-accounts/current-account-view/notes-tab/notes-tab.component.ts @@ -14,7 +14,7 @@ import { EntityNotesTabComponent } from '../../../shared/tabs/entity-notes-tab/e import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ - selector: 'mifosx-notes-tab', + selector: 'mifosx-current-account-notes-tab', templateUrl: './notes-tab.component.html', styleUrls: ['./notes-tab.component.scss'], imports: [ diff --git a/src/app/groups/groups-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts b/src/app/groups/groups-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts index ff7f33a25..e3f4ac200 100644 --- a/src/app/groups/groups-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts +++ b/src/app/groups/groups-view/custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component.ts @@ -23,7 +23,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; * Unassign staff dialog component. */ @Component({ - selector: 'mifosx-unassign-staff-dialog', + selector: 'mifosx-groups-unassign-staff-dialog', templateUrl: './unassign-staff-dialog.component.html', styleUrls: ['./unassign-staff-dialog.component.scss'], imports: [ diff --git a/src/app/groups/groups-view/notes-tab/notes-tab.component.ts b/src/app/groups/groups-view/notes-tab/notes-tab.component.ts index f46867649..8965842c9 100644 --- a/src/app/groups/groups-view/notes-tab/notes-tab.component.ts +++ b/src/app/groups/groups-view/notes-tab/notes-tab.component.ts @@ -22,7 +22,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; * Groups Notes Tab Component. */ @Component({ - selector: 'mifosx-notes-tab', + selector: 'mifosx-groups-notes-tab', templateUrl: './notes-tab.component.html', styleUrls: ['./notes-tab.component.scss'], imports: [ diff --git a/src/app/loans/loans-view/notes-tab/notes-tab.component.ts b/src/app/loans/loans-view/notes-tab/notes-tab.component.ts index 0905473d5..09ac10f57 100644 --- a/src/app/loans/loans-view/notes-tab/notes-tab.component.ts +++ b/src/app/loans/loans-view/notes-tab/notes-tab.component.ts @@ -18,7 +18,7 @@ import { EntityNotesTabComponent } from '../../../shared/tabs/entity-notes-tab/e import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ - selector: 'mifosx-notes-tab', + selector: 'mifosx-loans-notes-tab', templateUrl: './notes-tab.component.html', styleUrls: ['./notes-tab.component.scss'], imports: [ diff --git a/src/app/login/two-factor-authentication/two-factor-authentication.component.ts b/src/app/login/two-factor-authentication/two-factor-authentication.component.ts index 768cda2cd..c8d6dface 100644 --- a/src/app/login/two-factor-authentication/two-factor-authentication.component.ts +++ b/src/app/login/two-factor-authentication/two-factor-authentication.component.ts @@ -51,8 +51,18 @@ export class TwoFactorAuthenticationComponent implements OnInit, OnDestroy { otpRequested = false; /** Time for which OTP is valid. */ tokenValidityTime: number; - /** Two factor authentication delivery method form group. */ - twoFactorAuthenticationDeliveryMethodForm: UntypedFormGroup; + /** + * Two factor authentication delivery method form group. Initialised at field declaration so + * the template can bind to it on the very first change-detection cycle — otherwise Angular's + * standalone-signal renderer fires a "Cannot find control" error on the first pass before + * ngOnInit has had a chance to build the group, even though the form works fine after init. + */ + twoFactorAuthenticationDeliveryMethodForm: UntypedFormGroup = new UntypedFormBuilder().group({ + twoFactorAuthenticationDeliveryMethod: [ + '', + Validators.required + ] + }); /** Two factor authentication form group. */ twoFactorAuthenticationForm: UntypedFormGroup; /** True if loading. */ @@ -81,7 +91,7 @@ export class TwoFactorAuthenticationComponent implements OnInit, OnDestroy { * Gets the delivery methods available to receive OTP. */ ngOnInit() { - this.createTwoFactorAuthenticationDeliveryMethodForm(); + // Delivery-method form is initialised at field declaration — no need to call create() here. // Fetch SMS/EMAIL delivery methods and the user's ACTIVE DSCs in parallel, // merge them into a single factor list, then auto-route if there's only one. this.authenticationService.getDeliveryMethods().subscribe({ diff --git a/src/app/savings/savings-account-view/notes-tab/notes-tab.component.ts b/src/app/savings/savings-account-view/notes-tab/notes-tab.component.ts index e419b900b..3b3510bd8 100644 --- a/src/app/savings/savings-account-view/notes-tab/notes-tab.component.ts +++ b/src/app/savings/savings-account-view/notes-tab/notes-tab.component.ts @@ -14,7 +14,7 @@ import { EntityNotesTabComponent } from '../../../shared/tabs/entity-notes-tab/e import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ - selector: 'mifosx-notes-tab', + selector: 'mifosx-savings-notes-tab', templateUrl: './notes-tab.component.html', styleUrls: ['./notes-tab.component.scss'], imports: [