diff --git a/apps/36-blocks-widget/src/app/app.component.ts b/apps/36-blocks-widget/src/app/app.component.ts index 2290db1b..ad5a8c72 100644 --- a/apps/36-blocks-widget/src/app/app.component.ts +++ b/apps/36-blocks-widget/src/app/app.component.ts @@ -61,30 +61,30 @@ export class AppComponent extends BaseComponent implements OnInit, OnDestroy { console.log('Failure reason:', error); }, }; - // if (TYPE) { - // widgetConfig['type'] = TYPE; + if (TYPE) { + widgetConfig['type'] = TYPE; - // if (TYPE === PublicScriptType.Authorization) { - // // Optional: Path to redirect after login (e.g., '/login') only used get proxy_auth_token in admin panel while preview - // // widgetConfig['redirect_path'] = '/login'; - // // Used to show Company details in the signup/registration form, default is true - // // showCompanyDetails: false, - // } else { - // widgetConfig['authToken'] = AUTH_TOKEN; + if (TYPE === PublicScriptType.Authorization) { + // Optional: Path to redirect after login (e.g., '/login') only used get proxy_auth_token in admin panel while preview + // widgetConfig['redirect_path'] = '/login'; + // Used to show Company details in the signup/registration form, default is true + // showCompanyDetails: false, + } else { + widgetConfig['authToken'] = AUTH_TOKEN; - // if (TYPE === PublicScriptType.UserManagement) { - // // Enables the Role & Permission tab in the User Management widget - // widgetConfig['isRolePermission'] = false; - // } + if (TYPE === PublicScriptType.UserManagement) { + // Enables the Role & Permission tab in the User Management widget + widgetConfig['isRolePermission'] = false; + } - // // Note: Currently Subscription widget is not in use. - // if (TYPE === PublicScriptType.Subscription) { - // // Use in Subscription widget to redirect - // // loginRedirectUrl: 'https://www.google.com', - // // widgetConfig['isPreview'] = true; - // } - // } - // } + // Note: Currently Subscription widget is not in use. + if (TYPE === PublicScriptType.Subscription) { + // Use in Subscription widget to redirect + // loginRedirectUrl: 'https://www.google.com', + // widgetConfig['isPreview'] = true; + } + } + } if (THEME) { widgetConfig['theme'] = THEME; } diff --git a/apps/36-blocks-widget/src/app/otp/component/register/register.component.html b/apps/36-blocks-widget/src/app/otp/component/register/register.component.html index ec3d16b2..19a8c32b 100644 --- a/apps/36-blocks-widget/src/app/otp/component/register/register.component.html +++ b/apps/36-blocks-widget/src/app/otp/component/register/register.component.html @@ -27,72 +27,224 @@ > - - - - -
-
+ + @if (!isRegisterFormOnly()) { +
+ +
+
+ +
+ @if (!isEmailOtpVerified) { + + } @else { + + Verified + + + } +
- @if (!isOtpVerified) { - - } @else { - - Verified - + } + + + @if (isEmailOtpSent && !isEmailOtpVerified) { +
+ + @if (emailOtpError) { +

{{ emailOtpError }}

+ } +
} + } @else { + + } + + +
+
+
+ +
+ @if (!isOtpVerified) { + + } @else { + + Verified + + + } +
+
@@ -105,15 +257,15 @@ @if (isOtpSent && !isOtpVerified) {
-
+
@@ -121,9 +273,9 @@ type="text" maxlength="1" formControlName="otp2" - (input)="onOtpInput($event, 'otp2', otp3)" - (keyup)="onOtpKeyup($event, 'otp2')" - (keydown)="onOtpKeydown($event, 'otp2', otp1)" + (input)="onOtpInput($event, 'otp2', otp3, 'mobile')" + (keyup)="onOtpKeyup($event, 'otp2', 'mobile')" + (keydown)="onOtpKeydown($event, 'otp2', otp1, 'mobile')" #otp2 class="w-input-otp" /> @@ -131,9 +283,9 @@ type="text" maxlength="1" formControlName="otp3" - (input)="onOtpInput($event, 'otp3', otp4)" - (keyup)="onOtpKeyup($event, 'otp3')" - (keydown)="onOtpKeydown($event, 'otp3', otp2)" + (input)="onOtpInput($event, 'otp3', otp4, 'mobile')" + (keyup)="onOtpKeyup($event, 'otp3', 'mobile')" + (keydown)="onOtpKeydown($event, 'otp3', otp2, 'mobile')" #otp3 class="w-input-otp" /> @@ -141,9 +293,9 @@ type="text" maxlength="1" formControlName="otp4" - (input)="onOtpInput($event, 'otp4')" - (keyup)="onOtpKeyup($event, 'otp4')" - (keydown)="onOtpKeydown($event, 'otp4', otp3)" + (input)="onOtpInput($event, 'otp4', null, 'mobile')" + (keyup)="onOtpKeyup($event, 'otp4', 'mobile')" + (keydown)="onOtpKeydown($event, 'otp4', otp3, 'mobile')" #otp4 class="w-input-otp" /> @@ -256,9 +408,15 @@ let-hint="hint" let-formControl="formControl" let-patternError="patternError" + let-disabled="disabled" + let-onInput="onInput" + let-hideLabel="hideLabel" + let-hideErrors="hideErrors" >
- + @if (!hideLabel) { + + }
@if (type === 'password' || type === 'confirm password') { }
- @if (formControl?.touched) { - @if (formControl.errors?.['required']) { - - } @else if (formControl.errors?.['minlengthWithSpace']) { - - } @else if (formControl.errors?.['noStartEndSpaces']) { - - } @else if (formControl.errors?.['min']; as minError) { - - } @else if (formControl.errors?.['max']; as maxError) { - - } @else if (formControl.errors?.['minlength']; as minLengthError) { - - } @else if (formControl.errors?.['maxlength']; as maxLengthError) { - - } @else if (formControl.errors?.['pattern']) { - - } @else if (formControl.errors?.['valueSameAsControl']) { - - } - } - @if (hint) { -

{{ hint }}

+ @if (!hideErrors) { + }
+ + + @if (formControl?.touched) { + @if (formControl.errors?.['required']) { + + } @else if (formControl.errors?.['minlengthWithSpace']) { + + } @else if (formControl.errors?.['noStartEndSpaces']) { + + } @else if (formControl.errors?.['min']; as minError) { + + } @else if (formControl.errors?.['max']; as maxError) { + + } @else if (formControl.errors?.['minlength']; as minLengthError) { + + } @else if (formControl.errors?.['maxlength']; as maxLengthError) { + + } @else if (formControl.errors?.['pattern']) { + + } @else if (formControl.errors?.['valueSameAsControl']) { + + } @else if (formControl.errors?.['otpVerificationFailed']) { + + } + } + @if (hint) { +

{{ hint }}

+ } +
+
+ @if (!hideErrors) { + + } +
+ + + @if (formControl.touched && !intlClass?.[key]?.[required ? 'isRequiredValidNumber' : 'isValidNumber']) { } diff --git a/apps/36-blocks-widget/src/app/otp/component/register/register.component.ts b/apps/36-blocks-widget/src/app/otp/component/register/register.component.ts index c2d7f7ca..77a8723f 100644 --- a/apps/36-blocks-widget/src/app/otp/component/register/register.component.ts +++ b/apps/36-blocks-widget/src/app/otp/component/register/register.component.ts @@ -44,6 +44,8 @@ import { } from '../../store/selectors'; import { IGetOtpRes } from '../../model/otp'; +type OtpChannel = 'mobile' | 'email'; + @Component({ selector: 'proxy-register', imports: [CommonModule, ReactiveFormsModule], @@ -115,6 +117,12 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O otp3: new FormControl(''), otp4: new FormControl(''), }); + public emailOtpForm = new FormGroup({ + otp1: new FormControl(''), + otp2: new FormControl(''), + otp3: new FormControl(''), + otp4: new FormControl(''), + }); public intlClass: { [key: string]: IntlPhoneLib } = {}; public apiError = new BehaviorSubject(null); @@ -130,13 +138,23 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O public isOtpSent: boolean = false; public isNumberChanged: boolean = false; public otpError: string = ''; - public otpVerificationToken: string = ''; + public isEmailOtpVerified: boolean = false; + public isEmailOtpSent: boolean = false; + public isEmailChanged: boolean = false; + public emailOtpError: string = ''; + public emailVerificationToken: string = ''; + public mobileVerificationToken: string = ''; // Resend OTP timer properties public resendTimer: number = 0; public canResendOtp: boolean = true; public lastSentMobileNumber: string = ''; + public emailResendTimer: number = 0; + public canResendEmailOtp: boolean = true; + public lastSentEmail: string = ''; private timerSubscription: Subscription; + private emailTimerSubscription: Subscription; + private pendingOtpChannel: OtpChannel = 'mobile'; public selectWidgetTheme$: Observable; public uiPreferences: any = {}; @@ -209,9 +227,18 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O this.registrationForm .get('user.mobile') .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((res) => { + .subscribe(() => { this.isOtpVerified = false; - this.otpError = ''; // Clear error when mobile number changes + this.otpError = ''; + this.mobileVerificationToken = ''; + }); + this.registrationForm + .get('user.email') + .valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.isEmailOtpVerified = false; + this.emailOtpError = ''; + this.emailVerificationToken = ''; }); this.registrationForm .get('user.password') @@ -223,25 +250,48 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O }); this.selectVerifyOtpV2Success$.pipe(takeUntil(this.destroy$)).subscribe((res) => { - this.isOtpVerified = res; - if (res) { - this.registrationForm.get('user.mobile').setErrors(null); - this.otpError = ''; // Clear error on successful verification + if (this.pendingOtpChannel === 'email') { + this.isEmailOtpVerified = res; + if (res) { + this.registrationForm.get('user.email').setErrors(null); + this.emailOtpError = ''; + } + } else { + this.isOtpVerified = res; + if (res) { + this.registrationForm.get('user.mobile').setErrors(null); + this.otpError = ''; + } } this.cdr.markForCheck(); }); this.selectVerifyOtpV2Data$.pipe(takeUntil(this.destroy$)).subscribe((res) => { - this.otpVerificationToken = res?.data?.otp_verification_token; + const token = res?.data?.otp_verification_token; + if (token) { + if (this.pendingOtpChannel === 'email') { + this.emailVerificationToken = token; + } else { + this.mobileVerificationToken = token; + } + } this.cdr.markForCheck(); }); this.selectGetOtpSuccess$.pipe(takeUntil(this.destroy$)).subscribe((res) => { - if (res) { + if (!res) { + return; + } + if (this.pendingOtpChannel === 'email') { + this.isEmailOtpSent = true; + this.startResendTimer('email'); + this.lastSentEmail = this.registrationForm.get('user.email').value; + this.isEmailChanged = true; + } else { this.isOtpSent = true; - this.startResendTimer(); + this.startResendTimer('mobile'); this.lastSentMobileNumber = this.registrationForm.get('user.mobile').value; this.isNumberChanged = true; - this.cdr.markForCheck(); } + this.cdr.markForCheck(); }); // Handle API errors (OTP verification, getOtp, resendOtp) @@ -260,10 +310,11 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O // Display error inline in the dialog this.apiError.next([errorMessage]); - // If OTP was sent and error occurs during verification, also show inline error - if (this.isOtpSent && !this.isOtpVerified) { + if (this.pendingOtpChannel === 'email' && this.isEmailOtpSent && !this.isEmailOtpVerified) { + this.emailOtpError = 'Please enter valid OTP'; + this.emailOtpForm.reset(); + } else if (this.isOtpSent && !this.isOtpVerified) { this.otpError = 'Please enter valid OTP'; - // Clear OTP form to allow user to retry this.otpForm.reset(); } } @@ -318,24 +369,47 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O public ngOnDestroy(): void { // Remove global paste event listener document.removeEventListener('paste', this.handleGlobalPaste.bind(this)); - this.stopResendTimer(); + this.stopResendTimer('mobile'); + this.stopResendTimer('email'); super.ngOnDestroy(); } - private startResendTimer(): void { + private startResendTimer(channel: OtpChannel): void { + if (channel === 'email') { + this.canResendEmailOtp = false; + this.emailResendTimer = 30; + this.emailTimerSubscription = interval(1000).subscribe(() => { + this.emailResendTimer--; + if (this.emailResendTimer <= 0) { + this.stopResendTimer('email'); + this.canResendEmailOtp = true; + } + this.cdr.detectChanges(); + }); + return; + } this.canResendOtp = false; this.resendTimer = 30; this.timerSubscription = interval(1000).subscribe(() => { this.resendTimer--; if (this.resendTimer <= 0) { - this.stopResendTimer(); + this.stopResendTimer('mobile'); this.canResendOtp = true; } this.cdr.detectChanges(); }); } - private stopResendTimer(): void { + private stopResendTimer(channel: OtpChannel): void { + if (channel === 'email') { + if (this.emailTimerSubscription) { + this.emailTimerSubscription.unsubscribe(); + this.emailTimerSubscription = null; + } + this.emailResendTimer = 0; + this.canResendEmailOtp = true; + return; + } if (this.timerSubscription) { this.timerSubscription.unsubscribe(); this.timerSubscription = null; @@ -345,31 +419,11 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O } public resendOtp(): void { - const mobileControl = this.registrationForm.get('user.mobile'); - const currentMobile = mobileControl.value; - - // Check if mobile number has changed - if (currentMobile !== this.lastSentMobileNumber) { - // If number changed, reset timer and allow immediate resend - this.stopResendTimer(); - this.canResendOtp = true; - this.lastSentMobileNumber = currentMobile; - } - - mobileControl.markAsTouched(); - const isMobileValid = this.intlClass['user']?.isRequiredValidNumber; + this.sendOtpForChannel('mobile', true); + } - if (mobileControl.valid && isMobileValid && this.canResendOtp) { - this.store.dispatch( - sendOtpAction({ - request: { - referenceId: this.referenceId(), - mobile: mobileControl.value, - authkey: environment.sendOtpAuthKey, - }, - }) - ); - } + public resendEmailOtp(): void { + this.sendOtpForChannel('email', true); } public initIntl(key: string): void { @@ -423,21 +477,24 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O ); } private resetFormState(): void { - // Reset OTP verification states this.isOtpVerified = false; this.isOtpSent = false; this.isNumberChanged = false; this.otpError = ''; this.lastSentMobileNumber = ''; + this.isEmailOtpVerified = false; + this.isEmailOtpSent = false; + this.isEmailChanged = false; + this.emailOtpError = ''; + this.lastSentEmail = ''; - // Reset forms this.registrationForm.reset(); this.otpForm.reset(); + this.emailOtpForm.reset(); - // Reset timer - this.stopResendTimer(); + this.stopResendTimer('mobile'); + this.stopResendTimer('email'); - // Reset API errors this.apiError.next(null); } @@ -485,6 +542,10 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O this.registrationForm.get('user.mobile').setErrors({ otpVerificationFailed: true }); return; } + if (!this.isRegisterFormOnly() && !this.isEmailOtpVerified) { + this.registrationForm.get('user.email').setErrors({ otpVerificationFailed: true }); + return; + } const formData = removeEmptyKeys(cloneDeep(this.registrationForm.getRawValue()), true); const state = JSON.parse( this.otpUtilityService.aesDecrypt( @@ -527,7 +588,12 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O .register({ proxy_state: encodedData, state: registrationState, - otp_verification_token: this.otpVerificationToken, + ...(this.emailVerificationToken && { + email_otp_verification_token: this.emailVerificationToken, + }), + ...(this.mobileVerificationToken && { + mobile_otp_verification_token: this.mobileVerificationToken, + }), }) .subscribe( (response) => { @@ -539,51 +605,118 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O ); } - public getOtp() { - if (this.registrationForm.get('user.mobile').errors?.otpVerificationFailed) { - this.registrationForm.get('user.mobile').setErrors(null); - } + public getOtp(): void { + this.sendOtpForChannel('mobile', false); + } - const mobileControl = this.registrationForm.get('user.mobile'); - if (mobileControl.invalid) { - return; - } - const isMobileValid = this.intlClass['user']?.isRequiredValidNumber; + public getEmailOtp(): void { + this.sendOtpForChannel('email', false); + } - if (mobileControl.valid && isMobileValid) { + private sendOtpForChannel(channel: OtpChannel, isResend: boolean): void { + if (channel === 'email') { + const emailControl = this.registrationForm.get('user.email'); + if (emailControl.errors?.otpVerificationFailed) { + emailControl.setErrors(null); + } + if (emailControl.invalid) { + emailControl.markAsTouched(); + return; + } + const currentEmail = emailControl.value; + if (isResend && currentEmail !== this.lastSentEmail) { + this.stopResendTimer('email'); + this.canResendEmailOtp = true; + this.lastSentEmail = currentEmail; + } + if (!this.canResendEmailOtp) { + return; + } + this.pendingOtpChannel = 'email'; this.store.dispatch( sendOtpAction({ request: { referenceId: this.referenceId(), - mobile: mobileControl.value, - authkey: environment.sendOtpAuthKey, + identifier: currentEmail, }, }) ); + return; } - } - public verifyOtp() { - const otpValues = this.otpForm.value; + const mobileControl = this.registrationForm.get('user.mobile'); + if (mobileControl.errors?.otpVerificationFailed) { + mobileControl.setErrors(null); + } + if (mobileControl.invalid) { + mobileControl.markAsTouched(); + return; + } + const isMobileValid = this.intlClass['user']?.isRequiredValidNumber; + if (!isMobileValid) { + mobileControl.markAsTouched(); + return; + } + const currentMobile = mobileControl.value; + if (isResend && currentMobile !== this.lastSentMobileNumber) { + this.stopResendTimer('mobile'); + this.canResendOtp = true; + this.lastSentMobileNumber = currentMobile; + } + if (!this.canResendOtp) { + return; + } + this.pendingOtpChannel = 'mobile'; + this.store.dispatch( + sendOtpAction({ + request: { + referenceId: this.referenceId(), + identifier: currentMobile, + }, + }) + ); + } + public verifyOtp(): void { + this.verifyOtpForChannel('mobile'); + } + + public verifyEmailOtp(): void { + this.verifyOtpForChannel('email'); + } + + private verifyOtpForChannel(channel: OtpChannel): void { + const form = channel === 'email' ? this.emailOtpForm : this.otpForm; + const identifierControl = + channel === 'email' ? this.registrationForm.get('user.email') : this.registrationForm.get('user.mobile'); + const otpValues = form.value; const otpArray = [otpValues.otp1, otpValues.otp2, otpValues.otp3, otpValues.otp4]; const otpString = otpArray.filter((val) => val && val.trim() !== '').join(''); if (otpString.length === 4) { + this.pendingOtpChannel = channel; this.store.dispatch( verifyOtpAction({ request: { referenceId: this.referenceId(), - mobile: mobileControl.value, + identifier: identifierControl.value, otp: otpString, - authkey: environment.sendOtpAuthKey, }, }) ); } } - public onOtpInput(event: any, controlName: string, nextInput?: HTMLInputElement) { + private getOtpForm(channel: OtpChannel): FormGroup { + return channel === 'email' ? this.emailOtpForm : this.otpForm; + } + + public onOtpInput( + event: any, + controlName: string, + nextInput?: HTMLInputElement, + channel: OtpChannel = 'mobile' + ): void { const input = event.target; let value = input.value; @@ -591,10 +724,13 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O value = value.replace(/\D/g, ''); input.value = value; } - this.otpForm.get(controlName).setValue(value); + this.getOtpForm(channel).get(controlName).setValue(value); - // Clear error when user starts typing - if (this.otpError) { + if (channel === 'email') { + if (this.emailOtpError) { + this.emailOtpError = ''; + } + } else if (this.otpError) { this.otpError = ''; } @@ -605,62 +741,60 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O } } - public onOtpPaste(event: any) { + public onOtpPaste(event: any, channel: OtpChannel = 'mobile'): void { event.preventDefault(); const pastedData = event.clipboardData.getData('text/plain'); const otpDigits = pastedData.replace(/\D/g, '').slice(0, 4).split(''); - + const form = this.getOtpForm(channel); const otpFields = ['otp1', 'otp2', 'otp3', 'otp4']; - otpFields.forEach((fieldName, index) => { + + otpFields.forEach((fieldName) => { const controlName = fieldName as 'otp1' | 'otp2' | 'otp3' | 'otp4'; - this.otpForm.get(controlName).setValue(''); + form.get(controlName).setValue(''); }); otpDigits.forEach((digit, index) => { if (index < 4) { const controlName = otpFields[index] as 'otp1' | 'otp2' | 'otp3' | 'otp4'; - this.otpForm.get(controlName).setValue(digit); + form.get(controlName).setValue(digit); } }); - // Clear error when user pastes OTP - if (this.otpError) { + if (channel === 'email') { + if (this.emailOtpError) { + this.emailOtpError = ''; + } + } else if (this.otpError) { this.otpError = ''; } this.cdr.detectChanges(); - const lastFilledIndex = Math.min(otpDigits.length - 1, 3); - setTimeout(() => { - const nextField = document.querySelector(`#otp${lastFilledIndex + 1}`) as HTMLInputElement; - if (nextField && lastFilledIndex < 3) { - nextField.focus(); - } else { - const currentField = document.querySelector(`#otp${lastFilledIndex + 1}`) as HTMLInputElement; - if (currentField) { - currentField.focus(); - } - } - }, 100); } - private handleGlobalPaste(event: ClipboardEvent) { + private handleGlobalPaste(event: ClipboardEvent): void { const target = event.target as HTMLElement; - if (target && target.closest('.otp-container')) { - this.onOtpPaste(event); + if (target?.closest('.mobile-otp-container')) { + this.onOtpPaste(event, 'mobile'); + } else if (target?.closest('.email-otp-container')) { + this.onOtpPaste(event, 'email'); } } - public onOtpKeyup(event: any, controlName: string) { + public onOtpKeyup(event: any, controlName: string, channel: OtpChannel = 'mobile'): void { const input = event.target; const value = input.value; - this.otpForm.get(controlName).setValue(value); + this.getOtpForm(channel).get(controlName).setValue(value); this.cdr.detectChanges(); } - public onOtpKeydown(event: any, controlName: string, prevInput?: HTMLInputElement) { + public onOtpKeydown( + event: any, + controlName: string, + prevInput?: HTMLInputElement, + channel: OtpChannel = 'mobile' + ): void { const input = event.target; - // Handle backspace if (event.key === 'Backspace' && !input.value && prevInput) { event.preventDefault(); prevInput.focus(); @@ -675,16 +809,27 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O this.registrationForm.get('user.mobile').setValue(value); this.otpForm.reset(); - // Check if mobile number has changed if (value !== this.lastSentMobileNumber) { - this.stopResendTimer(); + this.stopResendTimer('mobile'); this.canResendOtp = true; } this.cdr.detectChanges(); } } - public numberChanged() { + + public onEmailInput = (): void => { + this.isEmailOtpSent = false; + this.emailOtpForm.reset(); + const value = this.registrationForm.get('user.email').value; + if (value !== this.lastSentEmail) { + this.stopResendTimer('email'); + this.canResendEmailOtp = true; + } + this.cdr.detectChanges(); + }; + + public numberChanged(): void { this.isOtpSent = false; this.isOtpVerified = false; this.isNumberChanged = false; @@ -692,6 +837,14 @@ export class RegisterComponent extends BaseComponent implements AfterViewInit, O this.cdr.detectChanges(); } + public emailChanged(): void { + this.isEmailOtpSent = false; + this.isEmailOtpVerified = false; + this.isEmailChanged = false; + this.emailOtpForm.reset(); + this.cdr.detectChanges(); + } + public get primaryColor(): string | null { if (this.version() !== 'v2') { return null; diff --git a/apps/36-blocks-widget/src/app/otp/model/otp.ts b/apps/36-blocks-widget/src/app/otp/model/otp.ts index 20cc5e8c..b4713413 100644 --- a/apps/36-blocks-widget/src/app/otp/model/otp.ts +++ b/apps/36-blocks-widget/src/app/otp/model/otp.ts @@ -28,15 +28,34 @@ export interface IGetOtpRes { } export interface ISendOtpReq { - referenceId: string; + referenceId?: string; + identifier?: string; + mobile?: string; + origin?: string; variables?: any; - authkey?: string; + authToken?: string; } export interface IRetryOtpReq extends ISendOtpReq { reqId: string; } +export interface IVerifyOtpV2Req { + referenceId?: string; + identifier?: string; + mobile?: string; + otp: string; + origin?: string; + authToken?: string; +} + +export interface IRegisterReq { + proxy_state?: string; + state?: string; + email_verification_token?: string; + mobile_verification_token?: string; +} + export interface IVerifyOtpReq { tokenAuth: string; widgetId: string; diff --git a/apps/36-blocks-widget/src/app/otp/service/otp.service.ts b/apps/36-blocks-widget/src/app/otp/service/otp.service.ts index 7eb1beda..4d55a550 100644 --- a/apps/36-blocks-widget/src/app/otp/service/otp.service.ts +++ b/apps/36-blocks-widget/src/app/otp/service/otp.service.ts @@ -1,9 +1,17 @@ import { Inject, Injectable } from '@angular/core'; import { BaseResponse, ProxyBaseUrls } from '@proxy/models/root-models'; import { Observable, of } from 'rxjs'; -import { omit } from 'lodash-es'; import { map } from 'rxjs/operators'; -import { OtpResModel, ISendOtpReq, IRetryOtpReq, IVerifyOtpReq, IWidgetResponse, IGetWidgetData } from '../model/otp'; +import { + OtpResModel, + ISendOtpReq, + IRetryOtpReq, + IVerifyOtpReq, + IVerifyOtpV2Req, + IRegisterReq, + IWidgetResponse, + IGetWidgetData, +} from '../model/otp'; import { otpVerificationUrls } from './urls/otp-urls'; import { HttpWrapperService } from '@proxy/services/http-wrapper-no-auth'; import { environment } from 'apps/36-blocks-widget/src/environments/environment'; @@ -26,6 +34,22 @@ export class OtpService { @Inject(ProxyBaseUrls.ClientURL) private clientUrl: any ) {} + private setOtpRequestHeaders(request: { referenceId?: string; authToken?: string; origin?: string }): void { + if (request.authToken) { + this.options.headers['proxy_auth_token'] = request.authToken; + delete this.options.headers['reference_id']; + } else if (request.referenceId) { + this.options.headers['reference_id'] = request.referenceId; + delete this.options.headers['proxy_auth_token']; + } + const origin = + request.origin ?? + (typeof window !== 'undefined' ? `${window.location.origin}${window.location.pathname}` : undefined); + if (origin) { + this.options.headers['origin'] = origin; + } + } + public getWidgetData( requestId: string, payload?: { [key: string]: any } @@ -35,22 +59,17 @@ export class OtpService { } public sendOtp(request: ISendOtpReq): Observable { - const referenceId = request.referenceId; - this.options.headers['authkey'] = request.authkey; - return this.http.post( - otpVerificationUrls.sendOtp(this.baseUrl).replace(':referenceId', referenceId), - omit(request, 'referenceId'), - this.options - ); + this.setOtpRequestHeaders(request); + const body = { identifier: request.identifier ?? request.mobile }; + return this.http.post(otpVerificationUrls.sendOtp(this.baseUrl), body, this.options); } - public verifyOtpV2(request: any): Observable { - const referenceId = request.referenceId; - this.options.headers['authkey'] = request.authkey; - return this.http.post( - otpVerificationUrls.verifyOtpV2(this.baseUrl).replace(':referenceId', referenceId), - omit(request, 'referenceId'), - this.options - ); + public verifyOtpV2(request: IVerifyOtpV2Req): Observable { + this.setOtpRequestHeaders(request); + const body = { + identifier: request.identifier ?? request.mobile, + otp: request.otp, + }; + return this.http.post(otpVerificationUrls.verifyOtpV2(this.baseUrl), body, this.options); } public resendOtpService(request: IRetryOtpReq): Observable { @@ -74,7 +93,7 @@ export class OtpService { return this.http.get(requestUrl, params, this.options); } - public register(body: { proxy_state?: string; state?: string; otp_verification_token?: string }): Observable { + public register(body: IRegisterReq): Observable { return this.http.post(otpVerificationUrls.register(this.baseUrl), body, this.options); } @@ -136,11 +155,20 @@ export class OtpService { public updateUser( name: string, authToken: string, - mobile?: string + mobile?: string, + otpVerificationToken?: string ): Observable> { this.options.headers['proxy_auth_token'] = authToken; const url = otpVerificationUrls.updateUser(this.clientUrl); - return this.http.put(url, { user: { name, mobile } }, this.options); + const user: { name: string; mobile?: string } = { name }; + if (mobile !== undefined) { + user.mobile = mobile; + } + const body: { user: typeof user; otp_verification_token?: string } = { user }; + if (otpVerificationToken) { + body.otp_verification_token = otpVerificationToken; + } + return this.http.put(url, body, this.options); } public addUser(payload: any, authToken: string): Observable> { this.options.headers['proxy_auth_token'] = authToken; diff --git a/apps/36-blocks-widget/src/app/otp/service/urls/otp-urls.ts b/apps/36-blocks-widget/src/app/otp/service/urls/otp-urls.ts index 2481eb6d..05f0f564 100644 --- a/apps/36-blocks-widget/src/app/otp/service/urls/otp-urls.ts +++ b/apps/36-blocks-widget/src/app/otp/service/urls/otp-urls.ts @@ -2,8 +2,8 @@ import { createUrl } from '@proxy/service'; export const otpVerificationUrls = { getWidgetData: (baseUrl) => createUrl(baseUrl, ':referenceId/widget'), - sendOtp: (baseUrl) => createUrl(baseUrl, ':referenceId/otp/send'), - verifyOtpV2: (baseUrl) => createUrl(baseUrl, ':referenceId/otp/verify'), + sendOtp: (baseUrl) => createUrl(baseUrl, 'otp/send'), + verifyOtpV2: (baseUrl) => createUrl(baseUrl, 'otp/verify'), verifyOtp: (baseUrl) => createUrl(baseUrl, 'widget/verifyOtp'), resend: (baseUrl) => createUrl(baseUrl, 'widget/retryOtp'), register: (baseUrl) => createUrl(baseUrl, 'c/register?action=redirect'), diff --git a/apps/36-blocks-widget/src/app/otp/store/actions/otp.action.ts b/apps/36-blocks-widget/src/app/otp/store/actions/otp.action.ts index b907313e..70d2021a 100644 --- a/apps/36-blocks-widget/src/app/otp/store/actions/otp.action.ts +++ b/apps/36-blocks-widget/src/app/otp/store/actions/otp.action.ts @@ -58,7 +58,10 @@ export const leaveCompanyError = createAction( props<{ errors: string[]; errorResponse: any }>() ); -export const updateUser = createAction('[OTP] Update Field', props<{ name: string; authToken: string }>()); +export const updateUser = createAction( + '[OTP] Update Field', + props<{ name: string; authToken: string; mobile?: string; otpVerificationToken?: string }>() +); export const updateUserComplete = createAction('[OTP] Update User Success', props<{ response: any }>()); export const updateUserError = createAction( '[OTP] Update User Failure', diff --git a/apps/36-blocks-widget/src/app/otp/store/effects/otp.effects.ts b/apps/36-blocks-widget/src/app/otp/store/effects/otp.effects.ts index 887c8371..db99301f 100644 --- a/apps/36-blocks-widget/src/app/otp/store/effects/otp.effects.ts +++ b/apps/36-blocks-widget/src/app/otp/store/effects/otp.effects.ts @@ -225,8 +225,8 @@ export class OtpEffects { UpdateUser$ = createEffect(() => this.actions$.pipe( ofType(otpActions.updateUser), - switchMap(({ name, authToken }) => { - return this.otpService.updateUser(name, authToken).pipe( + switchMap(({ name, authToken, mobile, otpVerificationToken }) => { + return this.otpService.updateUser(name, authToken, mobile, otpVerificationToken).pipe( map((res: any) => { if (res.type !== 'error') { return otpActions.updateUserComplete({ diff --git a/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.html b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.html index c4000eb4..170faa7b 100644 --- a/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.html +++ b/apps/36-blocks-widget/src/app/otp/user-profile/user-profile.component.html @@ -287,7 +287,7 @@

Edit Profile

-
+
+
- + @if (!isEditingMobile()) { +
+
+ +
+ +
+ } @else { +

+ Enter number with country code, without + (e.g. 919876543210) +

+
+
+ +
+
+ @if (!isMobileOtpVerified) { + + } @else { + + Verified + + + } + +
+
+ @if (clientForm.get('mobile')?.touched && !isProfileMobileValid()) { + + } @else if (clientForm.get('mobile')?.errors?.otpVerificationFailed) { + + } + + @if (isNumberChanged || isMobileOtpVerified) { + + } + + @if (isMobileOtpSent && !isMobileOtpVerified) { +
+ + @if (otpError) { +

{{ otpError }}

+ } +
+ } + }
-
+ +
; public deleteCompany$: Observable; public update$: Observable; + public selectGetOtpInProcess$: Observable; + public selectGetOtpSuccess$: Observable; + public selectVerifyOtpV2InProcess$: Observable; + public selectVerifyOtpV2Success$: Observable; public previousName: string; + public previousMobile: string = ''; public errorMessage: string; public error$: Observable; public companyDetails; - // authToken: string = ''; + + public otpForm = new FormGroup({ + otp1: new FormControl(''), + otp2: new FormControl(''), + otp3: new FormControl(''), + otp4: new FormControl(''), + }); + + public isMobileOtpVerified = false; + public isMobileOtpSent = false; + public isNumberChanged = false; + public otpError = ''; + public resendTimer = 0; + public canResendOtp = true; + public lastSentMobileNumber = ''; + public otpVerificationToken = ''; + private timerSubscription: Subscription; clientForm = new FormGroup({ name: new FormControl('', [Validators.required, Validators.pattern(NAME_REGEX)]), @@ -94,6 +144,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After }); public readonly isEditing = signal(false); + public readonly isEditingMobile = signal(false); private store = inject>(Store); private readonly actions$ = inject(Actions); @@ -130,6 +181,26 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After ); this.update$ = this.store.pipe(select(updateSuccess), distinctUntilChanged(isEqual), takeUntil(this.destroy$)); this.error$ = this.store.pipe(select(error), distinctUntilChanged(isEqual), takeUntil(this.destroy$)); + this.selectGetOtpInProcess$ = this.store.pipe( + select(selectGetOtpInProcess), + distinctUntilChanged(isEqual), + takeUntil(this.destroy$) + ); + this.selectGetOtpSuccess$ = this.store.pipe( + select(selectGetOtpSuccess), + distinctUntilChanged(isEqual), + takeUntil(this.destroy$) + ); + this.selectVerifyOtpV2InProcess$ = this.store.pipe( + select(selectVerifyOtpV2InProcess), + distinctUntilChanged(isEqual), + takeUntil(this.destroy$) + ); + this.selectVerifyOtpV2Success$ = this.store.pipe( + select(selectVerifyOtpV2Success), + distinctUntilChanged(isEqual), + takeUntil(this.destroy$) + ); } ngAfterViewInit(): void { @@ -139,6 +210,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After } ngOnDestroy(): void { + this.stopResendTimer(); this.editDialogRef?.detach(); this.confirmDialogPortalRef?.detach(); this.toastPortalRef?.detach(); @@ -152,7 +224,9 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After this.companyDetails = res; this.clientForm.get('name').setValue(res?.name); this.clientForm.get('email').setValue(res?.email); - this.clientForm.get('mobile').setValue(res?.mobile ? res.mobile : '--Not Provided--'); + const mobile = res?.mobile && res.mobile !== '--Not Provided--' ? res.mobile : ''; + this.previousMobile = mobile; + this.clientForm.get('mobile').setValue(mobile); } }); @@ -162,6 +236,44 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After } }); + this.selectVerifyOtpV2Success$.pipe(takeUntil(this.destroy$)).subscribe((res) => { + this.isMobileOtpVerified = res; + if (res) { + this.clientForm.get('mobile').setErrors(null); + this.otpError = ''; + } + this.cdr.markForCheck(); + }); + + this.store + .pipe(select(selectVerifyOtpV2Data), distinctUntilChanged(isEqual), takeUntil(this.destroy$)) + .subscribe((res) => { + if (res?.data?.otp_verification_token) { + this.otpVerificationToken = res.data.otp_verification_token; + } + this.cdr.markForCheck(); + }); + + this.selectGetOtpSuccess$.pipe(takeUntil(this.destroy$)).subscribe((res) => { + if (res) { + this.isMobileOtpSent = true; + this.startResendTimer(); + this.lastSentMobileNumber = this.getMobileIdentifier(); + this.lockMobileInput(); + this.cdr.markForCheck(); + } + }); + + this.store + .pipe(select(selectApiErrorResponse), distinctUntilChanged(isEqual), takeUntil(this.destroy$)) + .subscribe((errorResponse) => { + if (errorResponse && this.isMobileOtpSent && !this.isMobileOtpVerified) { + this.otpError = 'Please enter valid OTP'; + this.otpForm.reset(); + this.cdr.markForCheck(); + } + }); + this.store.dispatch( getUserDetails({ request: this.authToken(), @@ -213,7 +325,256 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After this.store.dispatch(leaveCompany({ companyId, authToken: this.authToken() })); } + public get displayMobile(): string { + const value = this.clientForm.get('mobile')?.value; + if (!value || value === '--Not Provided--') { + return 'Not provided'; + } + return value; + } + + public startEditMobile(): void { + this.resetMobileOtpState(); + this.isEditingMobile.set(true); + const mobileControl = this.clientForm.get('mobile'); + mobileControl.enable(); + if (mobileControl.value === '--Not Provided--') { + mobileControl.setValue(''); + } + this.cdr.detectChanges(); + } + + public cancelEditMobile(): void { + this.isEditingMobile.set(false); + this.resetMobileOtpState(); + const mobileControl = this.clientForm.get('mobile'); + mobileControl.setValue(this.previousMobile); + mobileControl.disable(); + this.cdr.detectChanges(); + } + + public getMobileIdentifier(): string { + const mobileControl = this.clientForm.get('mobile'); + const value = mobileControl?.disabled ? mobileControl.getRawValue() : mobileControl?.value; + return this.normalizeMobileDigits(value ?? ''); + } + + public isProfileMobileValid(): boolean { + const digits = this.getMobileIdentifier(); + return PROFILE_MOBILE_REGEX.test(digits); + } + + public onMobileBlur(): void { + const mobileControl = this.clientForm.get('mobile'); + mobileControl?.markAsTouched(); + const digits = this.getMobileIdentifier(); + if (digits !== mobileControl?.value) { + mobileControl?.setValue(digits, { emitEvent: false }); + } + this.cdr.detectChanges(); + } + + public onMobileKeypress(event: KeyboardEvent): void { + const char = event.key; + if (char.length === 1 && !/[0-9+]/.test(char)) { + event.preventDefault(); + } + } + + public numberChanged(): void { + this.isMobileOtpSent = false; + this.isMobileOtpVerified = false; + this.otpVerificationToken = ''; + this.otpForm.reset(); + this.unlockMobileInput(); + this.cdr.detectChanges(); + } + + private lockMobileInput(): void { + this.isNumberChanged = true; + this.clientForm.get('mobile')?.disable({ emitEvent: false }); + } + + private unlockMobileInput(): void { + this.isNumberChanged = false; + if (this.isEditingMobile()) { + this.clientForm.get('mobile')?.enable({ emitEvent: false }); + } + } + + public getOtp(): void { + this.sendOtp(false); + } + + public resendOtp(): void { + this.sendOtp(true); + } + + private sendOtp(isResend: boolean): void { + const mobileControl = this.clientForm.get('mobile'); + if (mobileControl.errors?.otpVerificationFailed) { + mobileControl.setErrors(null); + } + mobileControl.markAsTouched(); + if (!this.isProfileMobileValid()) { + this.cdr.detectChanges(); + return; + } + const currentMobile = this.getMobileIdentifier(); + if (isResend && currentMobile !== this.lastSentMobileNumber) { + this.stopResendTimer(); + this.canResendOtp = true; + this.lastSentMobileNumber = currentMobile; + } + if (!this.canResendOtp) { + return; + } + this.store.dispatch( + sendOtpAction({ + request: { + authToken: this.authToken(), + identifier: currentMobile, + }, + }) + ); + } + + public verifyOtp(): void { + const otpValues = this.otpForm.value; + const otpArray = [otpValues.otp1, otpValues.otp2, otpValues.otp3, otpValues.otp4]; + const otpString = otpArray.filter((val) => val && val.trim() !== '').join(''); + + if (otpString.length === 4) { + this.store.dispatch( + verifyOtpAction({ + request: { + authToken: this.authToken(), + identifier: this.getMobileIdentifier(), + otp: otpString, + }, + }) + ); + } + } + + public onMobileInput(): void { + if (this.isNumberChanged) { + return; + } + this.isMobileOtpSent = false; + this.isMobileOtpVerified = false; + this.otpVerificationToken = ''; + this.otpForm.reset(); + const value = this.getMobileIdentifier(); + if (value !== this.lastSentMobileNumber) { + this.stopResendTimer(); + this.canResendOtp = true; + } + this.cdr.detectChanges(); + } + + public onOtpInput(event: Event, controlName: string, nextInput?: HTMLInputElement): void { + const input = event.target as HTMLInputElement; + let value = input.value; + if (!/^\d*$/.test(value)) { + value = value.replace(/\D/g, ''); + input.value = value; + } + this.otpForm.get(controlName).setValue(value); + if (this.otpError) { + this.otpError = ''; + } + this.cdr.detectChanges(); + if (value && nextInput) { + nextInput.focus(); + } + } + + public onOtpKeyup(event: Event, controlName: string): void { + const input = event.target as HTMLInputElement; + this.otpForm.get(controlName).setValue(input.value); + this.cdr.detectChanges(); + } + + public onOtpKeydown(event: KeyboardEvent, prevInput?: HTMLInputElement): void { + const input = event.target as HTMLInputElement; + if (event.key === 'Backspace' && !input.value && prevInput) { + event.preventDefault(); + prevInput.focus(); + prevInput.select(); + } + } + + public onOtpPaste(event: ClipboardEvent): void { + event.preventDefault(); + const pastedData = event.clipboardData?.getData('text/plain') ?? ''; + const otpDigits = pastedData.replace(/\D/g, '').slice(0, 4).split(''); + const otpFields = ['otp1', 'otp2', 'otp3', 'otp4'] as const; + otpFields.forEach((field) => this.otpForm.get(field).setValue('')); + otpDigits.forEach((digit, index) => { + if (index < 4) { + this.otpForm.get(otpFields[index]).setValue(digit); + } + }); + if (this.otpError) { + this.otpError = ''; + } + this.cdr.detectChanges(); + } + + private startResendTimer(): void { + this.canResendOtp = false; + this.resendTimer = 30; + this.timerSubscription = interval(1000).subscribe(() => { + this.resendTimer--; + if (this.resendTimer <= 0) { + this.stopResendTimer(); + this.canResendOtp = true; + } + this.cdr.detectChanges(); + }); + } + + private stopResendTimer(): void { + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + this.timerSubscription = null; + } + this.resendTimer = 0; + this.canResendOtp = true; + } + + private resetMobileOtpState(): void { + this.isMobileOtpVerified = false; + this.isMobileOtpSent = false; + this.otpError = ''; + this.lastSentMobileNumber = ''; + this.otpVerificationToken = ''; + this.otpForm.reset(); + this.stopResendTimer(); + this.unlockMobileInput(); + this.store.dispatch( + resetAnyState({ + request: { + getOtpInProcess: false, + getOtpSuccess: false, + verifyOtpV2InProcess: false, + verifyOtpV2Success: false, + apiErrorResponse: null, + errors: null, + }, + }) + ); + } + + private normalizeMobileDigits(mobile: string): string { + return (mobile ?? '').replace(/\D/g, ''); + } + public openEditDialog(): void { + this.isEditingMobile.set(false); + this.resetMobileOtpState(); + this.clientForm.get('mobile').disable(); this.isEditing.set(true); this.cdr.detectChanges(); setTimeout(() => { @@ -231,20 +592,43 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After this.editDialogRef?.detach(); this.editDialogRef = null; this.isEditing.set(false); + this.isEditingMobile.set(false); + this.resetMobileOtpState(); this.clientForm.get('name').setValue(this.previousName); + this.clientForm.get('mobile').setValue(this.previousMobile); + this.clientForm.get('mobile').disable(); } updateUser() { const nameControl = this.clientForm.get('name'); + const mobileControl = this.clientForm.get('mobile'); const enteredName = nameControl?.value?.trim(); - if (enteredName === this.previousName) { + const enteredMobile = this.isEditingMobile() ? this.getMobileIdentifier() : (mobileControl?.value ?? '').trim(); + const nameChanged = enteredName !== this.previousName; + const mobileChanged = this.isEditingMobile() && enteredMobile !== this.previousMobile; + + if (!nameChanged && !mobileChanged) { this.editDialogRef?.detach(); this.editDialogRef = null; this.isEditing.set(false); + this.isEditingMobile.set(false); + mobileControl.disable(); + return; + } + + if (nameChanged && (!enteredName || nameControl.invalid)) { + return; + } + + if (mobileChanged && enteredMobile && !this.isProfileMobileValid()) { + mobileControl.markAsTouched(); + this.cdr.detectChanges(); return; } - if (!enteredName || nameControl.invalid) { + if (mobileChanged && !this.isMobileOtpVerified) { + mobileControl.setErrors({ otpVerificationFailed: true }); + this.cdr.detectChanges(); return; } @@ -259,7 +643,16 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After this.editDialogRef?.detach(); this.editDialogRef = null; this.isEditing.set(false); - this.previousName = enteredName; + this.isEditingMobile.set(false); + this.resetMobileOtpState(); + if (nameChanged) { + this.previousName = enteredName; + } + if (mobileChanged) { + this.previousMobile = enteredMobile; + } + mobileControl.setValue(this.previousMobile); + mobileControl.disable(); this.cdr.detectChanges(); this.toastService.success('Information successfully updated'); } @@ -269,9 +662,20 @@ export class UserProfileComponent extends BaseComponent implements OnInit, After if (err?.[0]) this.toastService.error(err[0]); }); - this.store.dispatch(updateUser({ name: enteredName, authToken: this.authToken() })); + this.store.dispatch( + updateUser({ + name: enteredName || this.previousName, + authToken: this.authToken(), + ...(mobileChanged && { + mobile: enteredMobile, + otpVerificationToken: this.otpVerificationToken, + }), + }) + ); - window.parent.postMessage({ type: 'proxy', data: { event: 'userNameUpdated', enteredName: enteredName } }, '*'); + if (nameChanged) { + window.parent.postMessage({ type: 'proxy', data: { event: 'userNameUpdated', enteredName } }, '*'); + } } public clear() { diff --git a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html index c32340b4..e1086b39 100644 --- a/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html +++ b/apps/36-blocks/src/app/panel/features/create-feature/create-feature.component.html @@ -1120,6 +1120,57 @@

+ + Allowed Domains & IPs + + @for (item of chipListValues[authorizationOriginsChipKey]; track item) { + + {{ item }} + + + } + + + Press Enter to add. e.g. domain.in, example.com, 192.168.1.1 + @if ( + featureForm.get('authorizationDetails.origins')?.hasError('atleastOneValueInChipList') && + featureForm.get('authorizationDetails.origins')?.touched + ) { + At least one domain or IP is required + } +