feat(users): OTP delivery method picker on user creation + post-login agreement
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s

Create User dialog now has a mandatory Email/Mobile OTP delivery method
picker (styled as a pill toggle) alongside the existing fields.

Adds a new post-login agreement acceptance dialog: right after the existing
2FA flow (TwoFactorAuthenticationComponent) succeeds, or immediately after
login for users without 2FA, the user must accept the bank's current
agreement/disclaimer at least once before reaching the app.

Also adds a "Manage User Agreement" admin screen (System section) to publish
versioned agreement text without a code deploy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 16:20:46 +05:30
co-authored by Claude Sonnet 5
parent d0536dbb11
commit d334dd1669
18 changed files with 1356 additions and 16 deletions
+38
View File
@@ -43,6 +43,7 @@ import { TenantSelectorComponent } from '../shared/tenant-selector/tenant-select
import { LoginFormComponent } from './login-form/login-form.component';
import { ResetPasswordComponent } from './reset-password/reset-password.component';
import { TwoFactorAuthenticationComponent } from './two-factor-authentication/two-factor-authentication.component';
import { AgreementAcceptComponent } from './agreement-accept/agreement-accept.component';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -85,6 +86,7 @@ import { AuthenticationService } from '../core/authentication/authentication.ser
LoginFormComponent,
ResetPasswordComponent,
TwoFactorAuthenticationComponent,
AgreementAcceptComponent,
MatMenuTrigger,
FaIconComponent,
MatMenu,
@@ -220,6 +222,7 @@ export class LoginComponent implements OnInit, OnDestroy {
private themingService = inject(ThemingService);
private dialog = inject(MatDialog);
private twoFactorDialogRef: MatDialogRef<TwoFactorAuthenticationComponent> | null = null;
private agreementAcceptDialogRef: MatDialogRef<AgreementAcceptComponent> | null = null;
private router = inject(Router);
private authService = inject(AuthenticationService);
@@ -245,6 +248,8 @@ export class LoginComponent implements OnInit, OnDestroy {
resetPassword = false;
/** True if user requires two factor authentication. */
twoFactorAuthenticationRequired = false;
/** True if the signed-in user still needs to accept the current agreement/disclaimer. */
agreementAcceptanceRequired = false;
/** Subscription to alerts. */
alert$: Subscription;
logoPath = 'assets/images/cbsnexora-logo.png';
@@ -275,16 +280,24 @@ export class LoginComponent implements OnInit, OnDestroy {
const alertType = alertEvent.type;
if (alertType === 'Password Expired') {
this.twoFactorAuthenticationRequired = false;
this.agreementAcceptanceRequired = false;
this.closeTwoFactorDialog();
this.closeAgreementAcceptDialog();
this.resetPassword = true;
} else if (alertType === 'Two Factor Authentication Required') {
this.resetPassword = false;
this.twoFactorAuthenticationRequired = true;
this.openTwoFactorDialog();
} else if (alertType === 'Agreement Acceptance Required') {
this.resetPassword = false;
this.agreementAcceptanceRequired = true;
this.openAgreementAcceptDialog();
} else if (alertType === 'Authentication Success') {
this.resetPassword = false;
this.twoFactorAuthenticationRequired = false;
this.agreementAcceptanceRequired = false;
this.closeTwoFactorDialog();
this.closeAgreementAcceptDialog();
// Cashier users go directly to the full-screen transaction entry dashboard
const destination = this.authService.isCashierUser() ? ['/cashier/transaction-entry'] : ['/'];
this.router.navigate(destination, { replaceUrl: true });
@@ -334,6 +347,7 @@ export class LoginComponent implements OnInit, OnDestroy {
this.theme$.unsubscribe();
}
this.closeTwoFactorDialog();
this.closeAgreementAcceptDialog();
clearInterval(this.slideTimer);
}
@@ -361,6 +375,30 @@ export class LoginComponent implements OnInit, OnDestroy {
}
}
/** Open the post-login agreement acceptance dialog as a non-dismissable modal */
private openAgreementAcceptDialog(): void {
if (this.agreementAcceptDialogRef) return;
this.agreementAcceptDialogRef = this.dialog.open(AgreementAcceptComponent, {
width: '460px',
maxWidth: '95vw',
panelClass: 'tfa-dialog-panel',
backdropClass: 'tfa-dialog-backdrop',
disableClose: true,
autoFocus: 'first-tabbable',
restoreFocus: true
});
this.agreementAcceptDialogRef.afterClosed().subscribe(() => {
this.agreementAcceptDialogRef = null;
});
}
private closeAgreementAcceptDialog(): void {
if (this.agreementAcceptDialogRef) {
this.agreementAcceptDialogRef.close();
this.agreementAcceptDialogRef = null;
}
}
goToSlide(index: number): void {
this.currentSlide = index;
this.restartSlider();