Skip to content

Commit d89b6e4

Browse files
feat(email-management): enhance email handling and error messaging
- Update ProfileEmailComponent to manage email input errors and provide user feedback for already linked emails. - Modify email verification modal to display error messages for invalid verification codes and handle pasting of digits. - Integrate NATS for fetching user emails and transform response for frontend compatibility. - Temporarily disable email deletion and primary email management features, with informative messages for users. Jira Ticket: https://linuxfoundation.atlassian.net/browse/LFXV2-502 Generated with [Cursor](https://cursor.com/) Signed-off-by: Mauricio Zanetti Salomao <mauriciozanetti86@gmail.com>
1 parent 2b12cdb commit d89b6e4

File tree

9 files changed

+438
-143
lines changed

9 files changed

+438
-143
lines changed

apps/lfx-one/src/app/modules/profile/email/email-verification-modal/email-verification-modal.component.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ <h2 class="text-xl font-semibold text-gray-900 mb-2">Verify Your Email Address</
5353
type="text"
5454
[id]="'digit' + digit"
5555
[formControlName]="'digit' + digit"
56-
maxlength="1"
5756
inputmode="numeric"
5857
pattern="\d"
5958
(input)="onDigitInput($event, digit)"
@@ -78,6 +77,15 @@ <h2 class="text-xl font-semibold text-gray-900 mb-2">Verify Your Email Address</
7877
</p>
7978
</div>
8079

80+
<!-- Error Message Display -->
81+
@if (errorMessage()) {
82+
<lfx-message severity="error" icon="fa-light fa-circle-exclamation" styleClass="mb-6">
83+
<ng-template #content>
84+
<p class="text-sm">{{ errorMessage() }}</p>
85+
</ng-template>
86+
</lfx-message>
87+
}
88+
8189
<!-- Resend Code Link -->
8290
<div class="mb-6 text-center">
8391
<p class="text-xs text-gray-600">

apps/lfx-one/src/app/modules/profile/email/email-verification-modal/email-verification-modal.component.ts

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export class EmailVerificationModalComponent implements OnInit, OnDestroy {
4646
});
4747

4848
public submitting = signal(false);
49+
public errorMessage = signal<string | null>(null);
4950

5051
public ngOnInit(): void {
5152
// Get email from dialog config
@@ -79,30 +80,123 @@ export class EmailVerificationModalComponent implements OnInit, OnDestroy {
7980
return;
8081
}
8182

83+
// Clear any previous error message
84+
this.errorMessage.set(null);
85+
8286
// Combine all 6 digits into a single code
8387
const code = `${this.verificationForm.value.digit1}${this.verificationForm.value.digit2}${this.verificationForm.value.digit3}${this.verificationForm.value.digit4}${this.verificationForm.value.digit5}${this.verificationForm.value.digit6}`;
8488
this.submitting.set(true);
8589

86-
// Simulate submission - in real implementation this would call an API
87-
setTimeout(() => {
88-
this.submitting.set(false);
89-
// Close the dialog and return the verification code
90-
this.dialogRef.close({ code, email: this.email });
91-
}, 500);
90+
// Call the verification API
91+
this.userService
92+
.verifyAndLinkEmail(this.email, code)
93+
.pipe(finalize(() => this.submitting.set(false)))
94+
.subscribe({
95+
next: () => {
96+
// On success, close the dialog and notify parent
97+
this.dialogRef.close({ success: true, email: this.email });
98+
},
99+
error: (error) => {
100+
console.error('Failed to verify code:', error);
101+
102+
// Check for specific error cases
103+
// ServiceValidationError structure: error.error.errors[0].message
104+
const specificMessage = error.error?.errors?.[0]?.message || '';
105+
const genericMessage = error.error?.message || error.message || '';
106+
107+
// Check both the specific validation message and generic message
108+
const errorText = (specificMessage + ' ' + genericMessage).toLowerCase();
109+
110+
let errorMsg = 'Invalid verification code. Please try again.';
111+
112+
if (errorText.includes('already linked')) {
113+
// Email is already linked to another account - close modal and signal parent
114+
this.dialogRef.close({
115+
alreadyLinked: true,
116+
email: this.email
117+
});
118+
return;
119+
}
120+
121+
// Show error message in the modal for other errors
122+
this.errorMessage.set(errorMsg);
123+
124+
// Clear the form so user can try again
125+
this.verificationForm.reset();
126+
127+
// Focus the first input
128+
setTimeout(() => {
129+
const firstInput = document.getElementById('digit1') as HTMLInputElement;
130+
if (firstInput) {
131+
firstInput.focus();
132+
}
133+
}, 100);
134+
},
135+
});
92136
}
93137

94138
public onDigitInput(event: Event, digitNumber: number): void {
95139
const input = event.target as HTMLInputElement;
96140
const value = input.value;
97141

98-
// Only allow single digit numbers
99-
if (value && !/^\d$/.test(value)) {
100-
input.value = '';
142+
// Check if this is a paste event (multiple characters)
143+
if (value.length > 1) {
144+
const digits = value.replace(/\D/g, '');
145+
146+
if (digits.length >= 6) {
147+
// This is a paste of 6 or more digits - fill all fields
148+
this.verificationForm.patchValue({
149+
digit1: digits[0] || '',
150+
digit2: digits[1] || '',
151+
digit3: digits[2] || '',
152+
digit4: digits[3] || '',
153+
digit5: digits[4] || '',
154+
digit6: digits[5] || '',
155+
});
156+
157+
// Clear the current input and focus the last field
158+
input.value = '';
159+
setTimeout(() => {
160+
const lastInput = document.getElementById('digit6') as HTMLInputElement;
161+
if (lastInput) {
162+
lastInput.focus();
163+
}
164+
}, 0);
165+
return;
166+
} else if (digits.length > 0) {
167+
// Partial paste - just take the first digit
168+
const firstDigit = digits[0];
169+
input.value = firstDigit;
170+
const controlName = `digit${digitNumber}` as 'digit1' | 'digit2' | 'digit3' | 'digit4' | 'digit5' | 'digit6';
171+
this.verificationForm.get(controlName)?.setValue(firstDigit);
172+
173+
// Move to next field
174+
if (digitNumber < 6) {
175+
const nextInput = document.getElementById(`digit${digitNumber + 1}`) as HTMLInputElement;
176+
if (nextInput) {
177+
nextInput.focus();
178+
nextInput.select();
179+
}
180+
}
181+
return;
182+
}
183+
}
184+
185+
// Filter out non-digit characters
186+
const filteredValue = value.replace(/\D/g, '');
187+
if (filteredValue !== value) {
188+
input.value = filteredValue;
189+
return;
190+
}
191+
192+
// Limit to single digit
193+
if (filteredValue.length > 1) {
194+
input.value = filteredValue[0];
101195
return;
102196
}
103197

104198
// If a digit was entered, move to next input
105-
if (value && digitNumber < 6) {
199+
if (filteredValue && digitNumber < 6) {
106200
const nextInput = document.getElementById(`digit${digitNumber + 1}`) as HTMLInputElement;
107201
if (nextInput) {
108202
nextInput.focus();
@@ -114,6 +208,13 @@ export class EmailVerificationModalComponent implements OnInit, OnDestroy {
114208
public onDigitKeyDown(event: KeyboardEvent, digitNumber: number): void {
115209
const input = event.target as HTMLInputElement;
116210

211+
// Handle Cmd+V (Mac) or Ctrl+V (Windows) paste
212+
if ((event.metaKey || event.ctrlKey) && event.key === 'v') {
213+
// Don't prevent default - let the paste happen
214+
// The onDigitInput handler will process it
215+
return;
216+
}
217+
117218
// Handle backspace - move to previous input if current is empty
118219
if (event.key === 'Backspace' && !input.value && digitNumber > 1) {
119220
event.preventDefault();
@@ -152,23 +253,52 @@ export class EmailVerificationModalComponent implements OnInit, OnDestroy {
152253

153254
public onDigitPaste(event: ClipboardEvent): void {
154255
event.preventDefault();
256+
event.stopPropagation();
257+
155258
const pastedData = event.clipboardData?.getData('text') || '';
156259
const digits = pastedData.replace(/\D/g, '').slice(0, 6);
157260

158-
if (digits.length > 0) {
159-
// Fill in the digits
261+
if (digits.length >= 6) {
262+
// If we have 6 digits, fill all fields from the beginning
263+
this.verificationForm.patchValue({
264+
digit1: digits[0],
265+
digit2: digits[1],
266+
digit3: digits[2],
267+
digit4: digits[3],
268+
digit5: digits[4],
269+
digit6: digits[5],
270+
});
271+
272+
// Focus the last input after filling all
273+
setTimeout(() => {
274+
const lastInput = document.getElementById('digit6') as HTMLInputElement;
275+
if (lastInput) {
276+
lastInput.focus();
277+
}
278+
}, 0);
279+
} else if (digits.length > 0) {
280+
// For partial pastes, fill sequentially from digit 1
281+
const fieldMap: { [key: number]: 'digit1' | 'digit2' | 'digit3' | 'digit4' | 'digit5' | 'digit6' } = {
282+
0: 'digit1',
283+
1: 'digit2',
284+
2: 'digit3',
285+
3: 'digit4',
286+
4: 'digit5',
287+
5: 'digit6',
288+
};
289+
160290
for (let i = 0; i < digits.length && i < 6; i++) {
161-
const controlName = `digit${i + 1}` as 'digit1' | 'digit2' | 'digit3' | 'digit4' | 'digit5' | 'digit6';
162-
this.verificationForm.get(controlName)?.setValue(digits[i]);
291+
this.verificationForm.get(fieldMap[i])?.setValue(digits[i]);
163292
}
164293

165-
// Focus the next empty input or the last one
166-
const nextEmptyIndex = digits.length < 6 ? digits.length + 1 : 6;
167-
const nextInput = document.getElementById(`digit${nextEmptyIndex}`) as HTMLInputElement;
168-
if (nextInput) {
169-
nextInput.focus();
170-
nextInput.select();
171-
}
294+
// Focus the next empty input
295+
setTimeout(() => {
296+
const nextIndex = Math.min(digits.length + 1, 6);
297+
const nextInput = document.getElementById(`digit${nextIndex}`) as HTMLInputElement;
298+
if (nextInput) {
299+
nextInput.focus();
300+
}
301+
}, 0);
172302
}
173303
}
174304

apps/lfx-one/src/app/modules/profile/email/profile-email.component.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
<ng-template #content>
1414
<div class="flex flex-col gap-1">
1515
<p class="text-sm">
16-
Add multiple email addresses to your account, set your primary email for account communications, and choose which emails receive specific types of
17-
notifications. Verified emails can be used to sign in to your account.
16+
View and manage the email addresses associated with your account.
1817
</p>
1918
</div>
2019
</ng-template>
@@ -41,7 +40,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="email-addresses-hea
4140
[disabled]="!email.canSetPrimary && !email.isPrimary"
4241
variant="text"
4342
size="small"
44-
[pTooltip]="email.isPrimary ? 'Primary email' : email.is_verified ? 'Set as primary email' : 'Verify email to set as primary'"
43+
[pTooltip]="email.isPrimary ? 'Primary email' : 'Alternate email'"
4544
tooltipPosition="top"
4645
[icon]="email.isPrimary ? 'fa-solid fa-star' : 'fa-regular fa-star'"
4746
[ngClass]="{
@@ -50,7 +49,7 @@ <h3 class="text-base font-medium text-gray-900" data-testid="email-addresses-hea
5049
'text-gray-300 cursor-not-allowed': !email.canSetPrimary && !email.isPrimary,
5150
}"
5251
[attr.data-testid]="'primary-icon-' + email.id"
53-
[attr.aria-label]="email.isPrimary ? 'Primary email' : email.is_verified ? 'Set as primary email' : 'Verify email to set as primary'">
52+
[attr.aria-label]="email.isPrimary ? 'Primary email' : 'Alternate email'">
5453
</lfx-button>
5554

5655
<div class="flex-1">
@@ -106,6 +105,12 @@ <h3 class="text-base font-medium text-gray-900" data-testid="email-addresses-hea
106105
}
107106
</div>
108107
}
108+
@if (emailFieldError()) {
109+
<div class="text-red-600 text-xs mt-1" data-testid="email-already-linked-error">
110+
<i class="fa-light fa-circle-exclamation mr-1"></i>
111+
{{ emailFieldError() }}
112+
</div>
113+
}
109114
</div>
110115
<lfx-button
111116
size="small"

0 commit comments

Comments
 (0)