feat(users): OTP delivery method picker on user creation + post-login agreement
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:
@@ -124,6 +124,19 @@
|
||||
<mat-error>Enter a valid <strong>10-digit mobile number</strong></mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="cu-checkbox-row cu-field--full">
|
||||
<div>
|
||||
<label class="cu-radio-label">OTP Delivery Method *</label>
|
||||
<mat-radio-group formControlName="otpDeliveryMethod">
|
||||
<mat-radio-button value="EMAIL">Email</mat-radio-button>
|
||||
<mat-radio-button value="SMS">Mobile</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
@if (userForm.controls.otpDeliveryMethod.hasError('required')) {
|
||||
<mat-error>Choose how this user will receive their first-login OTP.</mat-error>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -166,6 +166,14 @@ $gray-900: #202124;
|
||||
}
|
||||
}
|
||||
|
||||
.cu-radio-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: $gray-700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.cu-error {
|
||||
grid-column: 1 / -1;
|
||||
color: #dc2626;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ConfigurationWizardService } from 'app/configuration-wizard/configurati
|
||||
import { ContinueSetupDialogComponent } from 'app/configuration-wizard/continue-setup-dialog/continue-setup-dialog.component';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
@@ -41,7 +42,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatCheckbox,
|
||||
MatIcon
|
||||
MatIcon,
|
||||
MatRadioGroup,
|
||||
MatRadioButton
|
||||
]
|
||||
})
|
||||
export class CreateUserComponent implements OnInit, AfterViewInit {
|
||||
@@ -142,6 +145,10 @@ export class CreateUserComponent implements OnInit, AfterViewInit {
|
||||
roles: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
otpDeliveryMethod: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
},
|
||||
{ validator: confirmPasswordValidator }
|
||||
|
||||
@@ -72,6 +72,27 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!isEdit) {
|
||||
<div class="ufd-field">
|
||||
<label>OTP Delivery Method *</label>
|
||||
<div class="ufd-otp-methods" role="radiogroup" aria-label="OTP Delivery Method">
|
||||
<label class="ufd-otp-method" [class.is-selected]="form.value.otpDeliveryMethod === 'EMAIL'">
|
||||
<input type="radio" formControlName="otpDeliveryMethod" value="EMAIL" />
|
||||
<fa-icon icon="envelope"></fa-icon>
|
||||
<span>Email</span>
|
||||
</label>
|
||||
<label class="ufd-otp-method" [class.is-selected]="form.value.otpDeliveryMethod === 'SMS'">
|
||||
<input type="radio" formControlName="otpDeliveryMethod" value="SMS" />
|
||||
<fa-icon icon="mobile-alt"></fa-icon>
|
||||
<span>Mobile</span>
|
||||
</label>
|
||||
</div>
|
||||
@if (form.get('otpDeliveryMethod')?.touched && form.get('otpDeliveryMethod')?.errors?.['required']) {
|
||||
<div class="ufd-err">Choose how this user will receive their OTP.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Office mapping -->
|
||||
|
||||
@@ -126,6 +126,12 @@ $purple: #7c3aed;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 14px;
|
||||
|
||||
// A field placed right after the grid (e.g. OTP Delivery Method, which spans full width
|
||||
// and isn't part of the 2-column layout) needs its own breathing room above it.
|
||||
+ .ufd-field {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.ufd-field {
|
||||
@@ -268,6 +274,57 @@ $purple: #7c3aed;
|
||||
}
|
||||
}
|
||||
|
||||
.ufd-otp-methods {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ufd-otp-method {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
color: #475569;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
|
||||
fa-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line selector-no-qualifying-type
|
||||
input[type='radio'] {
|
||||
// Visually hidden but still focusable/clickable for accessibility - the pill itself
|
||||
// carries the selected state via .is-selected, matching .ufd-role's pattern.
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #94a3b8;
|
||||
color: $navy;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: rgb(30 136 229 / 12%);
|
||||
border-color: $blue;
|
||||
color: $blue-d;
|
||||
|
||||
fa-icon {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ufd-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -160,7 +160,11 @@ export class UserFormDialogComponent implements OnInit {
|
||||
Validators.required
|
||||
],
|
||||
passwordNeverExpires: [false],
|
||||
sendPasswordToEmail: [true]
|
||||
sendPasswordToEmail: [true],
|
||||
otpDeliveryMethod: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,6 +178,9 @@ export class UserFormDialogComponent implements OnInit {
|
||||
const selectedRoleIds = (u?.selectedRoles || []).map((r: any) => r.id);
|
||||
// Edit mode never asks about password — strip those controls.
|
||||
this.form.removeControl('sendPasswordToEmail');
|
||||
// OTP delivery method is a create-only concept (first-login gate) — changing an
|
||||
// existing user's channel post-creation is out of scope for this phase.
|
||||
this.form.removeControl('otpDeliveryMethod');
|
||||
this.form.patchValue({
|
||||
username: u.username,
|
||||
firstname: u.firstname,
|
||||
|
||||
Reference in New Issue
Block a user