Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b685d17
feat(meeting-join): implement automatic meeting join for authenticate…
jordane Oct 13, 2025
f200336
refactor(meeting-join): address PR feedback and improve code quality
jordane Oct 13, 2025
7ce877f
fix(meeting-join): prevent infinite auto-join loop and ensure new tab…
jordane Oct 13, 2025
9f6a472
style(meetings): enhance login button visibility with brand colors
jordane Oct 15, 2025
b025caf
feat(dashboard): initial core persona developer dashboard implementat…
jordane Oct 22, 2025
c23a382
fix(meeting-join): refactor auto-join logic to prevent infinite loop …
asithade Oct 22, 2025
4e60706
feat(dashboards): add maintainer persona with dashboard restructuring…
asithade Oct 22, 2025
a57b8dd
feat(email-verification): implement email verification flow with moda…
mauriciozanettisalomao Oct 24, 2025
89e54d7
feat(ui): comprehensive ui improvements and persona system enhancemen…
asithade Oct 23, 2025
f174b0d
feat(backend): add snowflake data warehouse integration (#129)
asithade Oct 24, 2025
7afef47
feat(email-management): enhance email handling and error messaging
mauriciozanettisalomao Oct 24, 2025
2c3d1b8
Merge branch 'main' into feature/lfxv2-502-email-linking-from-auth-se…
mauriciozanettisalomao Oct 24, 2025
39446f9
refactor(profile): improve error handling and logging in email verifi…
mauriciozanettisalomao Oct 24, 2025
fdfca42
Merge remote-tracking branch 'refs/remotes/origin/feature/lfxv2-502-e…
mauriciozanettisalomao Oct 29, 2025
eb05bab
fix(email-verification): enhance error message handling in submission…
mauriciozanettisalomao Oct 30, 2025
e6001d5
Merge branch 'main' into feature/lfxv2-502-email-linking-from-auth-se…
mauriciozanettisalomao Oct 30, 2025
3b3e6b1
refactor(email-verification): streamline verification code input and …
mauriciozanettisalomao Oct 31, 2025
3894895
refactor(profile-email): enhance email input handling and lifecycle m…
mauriciozanettisalomao Oct 31, 2025
12134c9
refactor(email-verification): remove debug log from error handling
mauriciozanettisalomao Oct 31, 2025
a8917d8
Merge branch 'main' into feature/lfxv2-502-email-linking-from-auth-se…
mauriciozanettisalomao Nov 10, 2025
480fa21
Merge branch 'main' into feature/lfxv2-502-email-linking-from-auth-se…
asithade Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="p-6">
<!-- Header -->
<div class="mb-6">
<h2 class="text-xl font-semibold text-gray-900 mb-2">Verify Your Email Address</h2>
<p class="text-sm text-gray-600">
We've sent a verification code to <strong class="text-gray-900">{{ email }}</strong>. Please enter the 6-digit code below to verify your email address.
</p>
</div>

<!-- Timer Display - Only show when not expired -->
@if (!timerExpired()) {
<div class="mb-6">
<div class="flex items-center justify-between p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div class="flex items-center gap-2">
<i class="fa-light fa-clock text-blue-600 text-lg"></i>
<span class="text-sm font-medium text-blue-900">Time Remaining</span>
</div>
<div class="flex items-center gap-2">
<span
class="text-2xl font-bold tabular-nums"
[ngClass]="{
'text-blue-600': timeRemaining() > 60,
'text-orange-600': timeRemaining() <= 60 && timeRemaining() > 30,
'text-red-600': timeRemaining() <= 30,
}">
{{ formattedTime() }}
</span>
</div>
</div>
</div>
}

<!-- Expired Message -->
@if (timerExpired()) {
<lfx-message severity="error" icon="fa-light fa-circle-exclamation" styleClass="mb-6">
<ng-template #content>
<p class="text-sm">The verification code has expired. Please request a new verification code.</p>
</ng-template>
</lfx-message>
}

<!-- Verification Code Form - Only show when not expired -->
@if (!timerExpired()) {
<form [formGroup]="verificationForm" (ngSubmit)="submitCode()">
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-3">Verification Code</label>
<div class="flex justify-center mb-3">
<p-inputOtp
formControlName="code"
[length]="6"
[integerOnly]="true"
[autofocus]="true"
styleClass="gap-3"
inputClass="w-12 h-14 text-center text-2xl font-bold"
data-testid="otp-input">
</p-inputOtp>
</div>
@if (verificationForm.get('code')?.invalid && (verificationForm.get('code')?.touched || verificationForm.get('code')?.dirty)) {
<div class="text-red-600 text-xs text-center mb-2" data-testid="verification-code-error">
Please enter all 6 digits
</div>
}
<p class="text-xs text-gray-500 text-center">
<i class="fa-light fa-info-circle mr-1"></i>
Enter the 6-digit code sent to your email address
</p>
</div>

<!-- Error Message Display -->
@if (errorMessage()) {
<lfx-message severity="error" icon="fa-light fa-circle-exclamation" styleClass="mb-6">
<ng-template #content>
<p class="text-sm">{{ errorMessage() }}</p>
</ng-template>
</lfx-message>
}

<!-- Resend Code Link -->
<div class="mb-6 text-center">
<p class="text-xs text-gray-600">
Didn't receive the code?
<button
type="button"
class="text-blue-600 hover:text-blue-700 font-medium ml-1 underline disabled:opacity-50 disabled:cursor-not-allowed"
(click)="resendCode()"
[disabled]="resending()"
data-testid="resend-link">
{{ resending() ? 'Sending...' : 'Resend code' }}
</button>
</p>
</div>

<!-- Action Buttons -->
<div class="flex justify-end gap-3">
<lfx-button
type="button"
label="Cancel"
severity="secondary"
variant="outlined"
(onClick)="cancel()"
[disabled]="submitting()"
data-testid="cancel-button">
</lfx-button>
<lfx-button
type="submit"
label="Verify Email"
severity="primary"
icon="fa-light fa-check"
[disabled]="verificationForm.invalid"
[loading]="submitting()"
data-testid="verify-button">
</lfx-button>
</div>
</form>
}

<!-- Expired State Actions -->
@if (timerExpired()) {
<div class="flex justify-end gap-3">
<lfx-button
type="button"
label="Cancel"
severity="secondary"
variant="outlined"
(onClick)="cancel()"
data-testid="cancel-button-expired">
</lfx-button>
<lfx-button
type="button"
label="Resend Code"
severity="primary"
icon="fa-light fa-envelope"
(onClick)="resendCode()"
[loading]="resending()"
[disabled]="resending()"
data-testid="resend-code-button">
</lfx-button>
</div>
}
</div>

<!-- Toast messages -->
<p-toast position="top-right" />

Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, computed, inject, OnInit, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonComponent } from '@shared/components/button/button.component';
import { MessageComponent } from '@shared/components/message/message.component';
import { UserService } from '@shared/services/user.service';
import { MessageService } from 'primeng/api';
import { AutoFocusModule } from 'primeng/autofocus';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { InputOtpModule } from 'primeng/inputotp';
import { ToastModule } from 'primeng/toast';
import { finalize, interval, Subscription } from 'rxjs';

@Component({
selector: 'lfx-email-verification-modal',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, ButtonComponent, MessageComponent, ToastModule, InputOtpModule, AutoFocusModule],
providers: [MessageService],
templateUrl: './email-verification-modal.component.html',
})
export class EmailVerificationModalComponent implements OnInit {
private readonly dialogRef = inject(DynamicDialogRef);
private readonly config = inject(DynamicDialogConfig);
private readonly userService = inject(UserService);
private readonly messageService = inject(MessageService);
private readonly destroyRef = takeUntilDestroyed();
private timerSubscription: Subscription | null = null;

// Timer configuration (5 minutes = 300 seconds)
private readonly TIMER_DURATION = 300;
public timeRemaining = signal(this.TIMER_DURATION);
public timerExpired = signal(false);
public resending = signal(false);

// Computed signal for formatted time display
public formattedTime = computed(() => {
const minutes = Math.floor(this.timeRemaining() / 60);
const seconds = this.timeRemaining() % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
});

// Email passed from parent
public email = '';

// Form for verification code - single control with 6-digit value
public verificationForm = new FormGroup({
code: new FormControl('', [Validators.required, Validators.pattern(/^\d{6}$/)]),
});

public submitting = signal(false);
public errorMessage = signal<string | null>(null);

public ngOnInit(): void {
// Get email from dialog config
this.email = this.config.data?.email || '';

// Start the countdown timer
this.startTimer();

// Clear error message when user starts typing (not on reset)
this.verificationForm
.get('code')
?.valueChanges.pipe(this.destroyRef)
.subscribe((value) => {
// Only clear error if user is actually typing (value is not empty)
if (value) {
this.errorMessage.set(null);
}
});
}

public submitCode(): void {
// Clear any previous error message first, before validation
this.errorMessage.set(null);

if (this.verificationForm.invalid || this.submitting() || this.timerExpired()) {
return;
}

const code = this.verificationForm.value.code || '';
this.submitting.set(true);

// Call the verification API
this.userService
.verifyAndLinkEmail(this.email, code)
.pipe(finalize(() => this.submitting.set(false)))
.subscribe({
next: () => {
// On success, close the dialog and notify parent
this.dialogRef.close({ success: true, email: this.email });
},
error: (error) => {
console.error('Failed to verify code:', error);

// Check for specific error cases
// ServiceValidationError structure: error.error.errors[0].message
const specificMessage = error.error?.errors?.[0]?.message || '';
const genericMessage = error.error?.message || error.message || '';

// Check for microservice error format
const errorCode = error.error?.error_code || '';
const errorType = error.error?.error_type || '';

// Check both the specific validation message and generic message
const errorText = (specificMessage + ' ' + genericMessage + ' ' + errorCode).toLowerCase();

let errorMsg = 'Invalid verification code. Please try again.';

// Handle specific error cases
if (errorText.includes('already linked')) {
// Email is already linked to another account - close modal and signal parent
this.dialogRef.close({
alreadyLinked: true,
email: this.email
});
return;
}

// Handle OTP verification failed
if (errorCode === 'OTP_VERIFICATION_FAILED' || errorType === '_MicroserviceError') {
errorMsg = 'Invalid or expired verification code. Please try again.';
}

// Clear the form first (this triggers valueChanges which would clear error message)
this.verificationForm.reset();

// Set error message AFTER reset to prevent it from being cleared
this.errorMessage.set(errorMsg);
},
});
}

public cancel(): void {
this.dialogRef.close(null);
}

private startTimer(): void {
// Cancel any existing timer to prevent multiple concurrent intervals
if (this.timerSubscription) {
this.timerSubscription.unsubscribe();
}

this.timerSubscription = interval(1000)
.pipe(this.destroyRef)
.subscribe(() => {
const remaining = this.timeRemaining() - 1;

if (remaining <= 0) {
this.timeRemaining.set(0);
this.timerExpired.set(true);
this.verificationForm.disable();
// Timer will auto-cleanup on component destroy
} else {
this.timeRemaining.set(remaining);
}
});
}

public resendCode(): void {
this.resending.set(true);

// Call the backend to resend verification code
this.userService
.sendEmailVerification(this.email)
.pipe(finalize(() => this.resending.set(false)))
.subscribe({
next: () => {
// Reset the timer and form
this.timeRemaining.set(this.TIMER_DURATION);
this.timerExpired.set(false);
this.verificationForm.enable();
this.verificationForm.reset();

// Clear any previous error message
this.errorMessage.set(null);

// Restart the timer
this.startTimer();

// Show success message
this.messageService.add({
severity: 'success',
summary: 'Code Sent',
detail: 'A new verification code has been sent to your email',
});
},
error: (error) => {
console.error('Failed to resend verification code:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to send verification code. Please try again.',
});
},
});
}
}

Loading