Skip to content
Merged

Stage #632

Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -13,11 +13,13 @@ import {
OnInit,
ViewChild,
ElementRef,
afterNextRender,
computed,
effect,
inject,
input,
output,
Injector,
} from '@angular/core';
import { resetAll, resetAnyState, sendOtpAction, verifyOtpAction } from '../../store/actions/otp.action';
import { BaseComponent } from '@proxy/ui/base-component';
Expand Down Expand Up @@ -163,11 +165,13 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
@ViewChild('otp2', { static: false }) otp2Ref: ElementRef;
@ViewChild('otp3', { static: false }) otp3Ref: ElementRef;
@ViewChild('otp4', { static: false }) otp4Ref: ElementRef;
@ViewChild('emailOtp1', { static: false }) emailOtp1Ref: ElementRef;

private store = inject<Store<IAppState>>(Store);
private otpService = inject(OtpService);
private otpUtilityService = inject(OtpUtilityService);
private cdr = inject(ChangeDetectorRef);
private readonly injector = inject(Injector);
private readonly themeService = inject(WidgetThemeService);
private readonly el = inject(ElementRef);
readonly isDarkTheme = computed(() => this.themeService.isDark$());
Expand Down Expand Up @@ -280,7 +284,8 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
if (!res) {
return;
}
if (this.pendingOtpChannel === 'email') {
const channel = this.pendingOtpChannel;
if (channel === 'email') {
this.isEmailOtpSent = true;
this.startResendTimer('email');
this.lastSentEmail = this.registrationForm.get('user.email').value;
Expand All @@ -291,7 +296,8 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
this.lastSentMobileNumber = this.registrationForm.get('user.mobile').value;
this.isNumberChanged = true;
}
this.cdr.markForCheck();
this.cdr.detectChanges();
afterNextRender(() => this.focusFirstOtpInput(channel), { injector: this.injector });
});

// Handle API errors (OTP verification, getOtp, resendOtp)
Expand Down Expand Up @@ -711,6 +717,15 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O
return channel === 'email' ? this.emailOtpForm : this.otpForm;
}

private focusFirstOtpInput(channel: OtpChannel): void {
const containerSelector = channel === 'email' ? '.email-otp-container' : '.mobile-otp-container';
const input =
(this.el.nativeElement.querySelector(`${containerSelector} input`) as HTMLInputElement) ||
(channel === 'email' ? this.emailOtp1Ref?.nativeElement : this.otp1Ref?.nativeElement);

input?.focus();
}

public onOtpInput(
event: any,
controlName: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ <h2 id="edit-profile-title" class="w-dialog-title">Edit Profile</h2>
autocomplete="off"
class="w-input"
[class.border-red-500]="
clientForm.get('mobile')?.touched && !isProfileMobileValid()
getOtpError ||
(clientForm.get('mobile')?.touched && !isProfileMobileValid())
"
(keypress)="onMobileKeypress($event)"
(input)="onMobileInput()"
Expand Down Expand Up @@ -390,10 +391,13 @@ <h2 id="edit-profile-title" class="w-dialog-title">Edit Profile</h2>
</button>
</div>
</div>
@if (clientForm.get('mobile')?.touched && !isProfileMobileValid()) {
<p role="alert" class="w-field-error">
Enter a valid mobile number with country code (10–15 digits).
</p>
@if (getOtpError) {
<p role="alert" class="w-field-error">{{ getOtpError }}</p>
} @else if (
clientForm.get('mobile')?.touched && getProfileMobileValidationError();
as mobileError
) {
<p role="alert" class="w-field-error">{{ mobileError }}</p>
} @else if (clientForm.get('mobile')?.errors?.otpVerificationFailed) {
<p role="alert" class="w-field-error">Please verify the mobile number.</p>
}
Expand All @@ -408,7 +412,10 @@ <h2 id="edit-profile-title" class="w-dialog-title">Edit Profile</h2>

@if (isMobileOtpSent && !isMobileOtpVerified) {
<div class="flex flex-col gap-2 mt-2">
<div class="flex items-center flex-wrap gap-3" [formGroup]="otpForm">
<div
class="flex items-center flex-wrap gap-3 mobile-otp-container"
[formGroup]="otpForm"
>
<input
type="text"
maxlength="1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
Component,
ElementRef,
Input,
Injector,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation,
afterNextRender,
computed,
effect,
inject,
Expand Down Expand Up @@ -64,9 +66,9 @@ import { isEqual } from 'lodash-es';
import { NAME_REGEX } from '@proxy/regex';
import { WidgetTheme } from '@proxy/constant';
import { WidgetThemeService } from '../service/widget-theme.service';
import { PhoneNumberUtil } from 'google-libphonenumber';

/** Digits only, 10–15 chars, with country code (e.g. 919876543210) */
const PROFILE_MOBILE_REGEX = /^[1-9]\d{9,14}$/;
const phoneUtil = PhoneNumberUtil.getInstance();

@Component({
selector: 'user-profile',
Expand Down Expand Up @@ -131,6 +133,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After
public isMobileOtpSent = false;
public isNumberChanged = false;
public otpError = '';
public getOtpError = '';
public resendTimer = 0;
public canResendOtp = true;
public lastSentMobileNumber = '';
Expand All @@ -151,11 +154,13 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After
readonly toastService = inject(ToastService);
private readonly widgetPortal = inject(WidgetPortalService);
private readonly cdr = inject(ChangeDetectorRef);
private readonly injector = inject(Injector);
readonly confirmDialogCompanyId = signal<number | null>(null);

@ViewChild('editDialogPortal') private editDialogPortalEl?: ElementRef<HTMLElement>;
@ViewChild('confirmDialogPortal') private confirmDialogPortalEl?: ElementRef<HTMLElement>;
@ViewChild('toastPortal') private toastPortalEl?: ElementRef<HTMLElement>;
@ViewChild('otp1', { static: false }) private otp1Ref?: ElementRef<HTMLInputElement>;

private editDialogRef: WidgetPortalRef | null = null;
private confirmDialogPortalRef: WidgetPortalRef | null = null;
Expand Down Expand Up @@ -257,21 +262,32 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After
this.selectGetOtpSuccess$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
if (res) {
this.isMobileOtpSent = true;
this.getOtpError = '';
this.startResendTimer();
this.lastSentMobileNumber = this.getMobileIdentifier();
this.lockMobileInput();
this.cdr.markForCheck();
this.cdr.detectChanges();
afterNextRender(() => this.focusFirstOtpInput(), { injector: this.injector });
}
});

this.store
.pipe(select(selectApiErrorResponse), distinctUntilChanged(isEqual), takeUntil(this.destroy$))
.subscribe((errorResponse) => {
if (errorResponse && this.isMobileOtpSent && !this.isMobileOtpVerified) {
this.otpError = 'Please enter valid OTP';
if (!errorResponse) {
return;
}

const errorMessage = this.extractApiErrorMessage(errorResponse);

if (this.isMobileOtpSent && !this.isMobileOtpVerified) {
this.otpError = errorMessage;
this.otpForm.reset();
this.cdr.markForCheck();
} else if (this.isEditingMobile()) {
this.getOtpError = errorMessage;
}

this.cdr.markForCheck();
});

this.store.dispatch(
Expand Down Expand Up @@ -360,8 +376,40 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After
}

public isProfileMobileValid(): boolean {
return this.getProfileMobileValidationError() === null;
}

public getProfileMobileValidationError(): string | null {
const digits = this.getMobileIdentifier();
return PROFILE_MOBILE_REGEX.test(digits);

if (!digits) {
return 'Mobile number is required.';
}

if (!/^\d+$/.test(digits)) {
return 'Mobile number must contain digits only.';
}

try {
const parsed = phoneUtil.parse(`+${digits}`, undefined);
const countryCode = parsed.getCountryCode();

if (!countryCode) {
return 'Enter a valid country code (e.g. 91 for India).';
}

if (!phoneUtil.isValidNumber(parsed)) {
const nationalNumber = parsed.getNationalNumber()?.toString() ?? '';
if (!nationalNumber) {
return 'Enter a valid country code with your mobile number.';
}
return 'Enter a valid mobile number for the country code.';
}

return null;
} catch {
return 'Enter a valid mobile number with country code (e.g. 919876543210).';
}
}

public onMobileBlur(): void {
Expand All @@ -376,7 +424,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After

public onMobileKeypress(event: KeyboardEvent): void {
const char = event.key;
if (char.length === 1 && !/[0-9+]/.test(char)) {
if (char.length === 1 && !/[0-9]/.test(char)) {
event.preventDefault();
}
}
Expand Down Expand Up @@ -429,6 +477,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After
if (!this.canResendOtp) {
return;
}
this.getOtpError = '';
this.store.dispatch(
sendOtpAction({
request: {
Expand Down Expand Up @@ -464,6 +513,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After
this.isMobileOtpSent = false;
this.isMobileOtpVerified = false;
this.otpVerificationToken = '';
this.getOtpError = '';
this.otpForm.reset();
const value = this.getMobileIdentifier();
if (value !== this.lastSentMobileNumber) {
Expand Down Expand Up @@ -544,10 +594,31 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After
this.canResendOtp = true;
}

private extractApiErrorMessage(errorResponse: any): string {
return (
errorResponse?.errors?.message ||
errorResponse?.data?.message ||
errorResponse?.error?.errors?.message ||
errorResponse?.error?.data?.message ||
errorResponse?.error?.message ||
errorResponse?.message ||
'An error occurred'
);
}

private focusFirstOtpInput(): void {
const input =
this.otp1Ref?.nativeElement ||
(this.editDialogPortalEl?.nativeElement.querySelector('.mobile-otp-container input') as HTMLInputElement);

input?.focus();
}

private resetMobileOtpState(): void {
this.isMobileOtpVerified = false;
this.isMobileOtpSent = false;
this.otpError = '';
this.getOtpError = '';
this.lastSentMobileNumber = '';
this.otpVerificationToken = '';
this.otpForm.reset();
Expand Down
Loading