From f7d7cd3a83b3a637a81c4e6e465334de07357ef4 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Fri, 12 Apr 2024 10:40:06 +0800 Subject: [PATCH 01/66] CXSPA-6672: add otp login --- feature-libs/user/_index.scss | 2 +- .../login-form/login-form.module.ts | 20 ++++- .../login-form/otp-login-form.component.ts | 78 +++++++++++++++++++ .../login-form/use-otp-login-form.ts | 14 ++++ .../account/core/connectors/converters.ts | 12 ++- .../core/connectors/user-account.adapter.ts | 9 ++- .../core/connectors/user-account.connector.ts | 9 ++- .../account/core/facade/facade-providers.ts | 13 +++- .../user/account/core/facade/index.ts | 1 + .../core/facade/verification-token.service.ts | 34 ++++++++ ...efault-occ-user-account-endpoint.config.ts | 7 +- .../config/occ-user-account-endpoint.model.ts | 5 ++ .../occ/adapters/occ-user-account.adapter.ts | 38 ++++++++- .../user/account/root/facade/index.ts | 1 + .../root/facade/verification-token.facade.ts | 26 +++++++ .../user/account/root/model/user.model.ts | 12 +++ feature-libs/user/account/styles/_index.scss | 1 + .../user/account/styles/_otp-login-form.scss | 10 +++ .../features/user/user-feature.module.ts | 6 ++ .../src/environments/environment.prod.ts | 1 + .../src/environments/environment.ts | 1 + .../models/build.process.env.d.ts | 1 + .../environments/models/environment.model.ts | 1 + 23 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 feature-libs/user/account/components/login-form/otp-login-form.component.ts create mode 100644 feature-libs/user/account/components/login-form/use-otp-login-form.ts create mode 100644 feature-libs/user/account/core/facade/verification-token.service.ts create mode 100644 feature-libs/user/account/root/facade/verification-token.facade.ts create mode 100644 feature-libs/user/account/styles/_otp-login-form.scss diff --git a/feature-libs/user/_index.scss b/feature-libs/user/_index.scss index e86984fedb2..b0b3c3c223a 100644 --- a/feature-libs/user/_index.scss +++ b/feature-libs/user/_index.scss @@ -8,7 +8,7 @@ $skipComponentStyles: () !default; $selectors: cx-address-book, cx-address-form, cx-suggested-addresses-dialog, cx-login, cx-login-form, cx-register, cx-reset-password, cx-close-account, cx-close-account-modal, cx-my-account-v2-profile, cx-my-account-v2-email, - cx-my-account-v2-password !default; + cx-my-account-v2-password, cx-otp-login-form !default; @each $selector in $selectors { #{$selector} { diff --git a/feature-libs/user/account/components/login-form/login-form.module.ts b/feature-libs/user/account/components/login-form/login-form.module.ts index f89dd4404c1..eff4bbd6892 100644 --- a/feature-libs/user/account/components/login-form/login-form.module.ts +++ b/feature-libs/user/account/components/login-form/login-form.module.ts @@ -5,7 +5,7 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { NgModule, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { @@ -18,6 +18,7 @@ import { UrlModule, WindowRef, provideDefaultConfig, + provideDefaultConfigFactory, } from '@spartacus/core'; import { FormErrorsModule, @@ -26,6 +27,17 @@ import { } from '@spartacus/storefront'; import { LoginFormComponentService } from './login-form-component.service'; import { LoginFormComponent } from './login-form.component'; +import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; + +import { USE_ONE_TIME_PASSWORD_LOGIN } from './use-otp-login-form'; + +const oneTimePasswordLoginFormMapping: CmsConfig = { + cmsComponents: { + ReturningCustomerLoginComponent: { + component: OneTimePasswordLoginFormComponent, + }, + }, +}; @NgModule({ imports: [ @@ -56,7 +68,11 @@ import { LoginFormComponent } from './login-form.component'; }, }, }), + provideDefaultConfigFactory(() => + inject(USE_ONE_TIME_PASSWORD_LOGIN) ? oneTimePasswordLoginFormMapping : {} + ), ], - declarations: [LoginFormComponent], + declarations: [LoginFormComponent, OneTimePasswordLoginFormComponent], + exports: [LoginFormComponent, OneTimePasswordLoginFormComponent], }) export class LoginFormModule {} diff --git a/feature-libs/user/account/components/login-form/otp-login-form.component.ts b/feature-libs/user/account/components/login-form/otp-login-form.component.ts new file mode 100644 index 00000000000..b98fb4b6cba --- /dev/null +++ b/feature-libs/user/account/components/login-form/otp-login-form.component.ts @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + Component, + HostBinding, + inject, +} from '@angular/core'; +import { + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { WindowRef } from '@spartacus/core'; +import { CustomFormValidators } from '@spartacus/storefront'; +import { BehaviorSubject, Observable, tap } from 'rxjs'; +import { VerificationTokenFacade } from '../../root/facade/verification-token.facade'; +import { LoginForm } from '../../root/model'; +import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from './use-otp-login-form'; + +@Component({ + selector: 'cx-otp-login-form', + templateUrl: './login-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OneTimePasswordLoginFormComponent { + protected verificationTokenFacade = inject(VerificationTokenFacade); + protected winRef = inject(WindowRef); + + protected busy$ = new BehaviorSubject(false); + + isUpdating$: Observable = this.busy$.pipe( + tap((state) => { + const userId = this.winRef.nativeWindow?.history?.state?.['newUid']; + if (userId) { + this.form.patchValue({ userId }); + } + state === true ? this.form.disable() : this.form.enable(); + }) + ); + + form: UntypedFormGroup = new UntypedFormGroup({ + userId: new UntypedFormControl('', [ + Validators.required, + CustomFormValidators.emailValidator, + ]), + password: new UntypedFormControl('', Validators.required), + }); + + @HostBinding('class.user-form') style = true; + + onSubmit(): void { + if (!this.form.valid) { + this.form.markAllAsTouched(); + return; + } + + this.busy$.next(true); + this.verificationTokenFacade.createVerificationToken( + this.collectDataFromLoginForm() + ); + this.busy$.next(false); + } + + collectDataFromLoginForm(): LoginForm { + return { + // TODO: consider dropping toLowerCase as this should not be part of the UI, + // as it's too opinionated and doesn't work with other AUTH services + loginId: this.form.value.userId.toLowerCase(), + password: this.form.value.password, + purpose: ONE_TIME_PASSWORD_LOGIN_PURPOSE, + }; + } +} diff --git a/feature-libs/user/account/components/login-form/use-otp-login-form.ts b/feature-libs/user/account/components/login-form/use-otp-login-form.ts new file mode 100644 index 00000000000..3f9e29cf811 --- /dev/null +++ b/feature-libs/user/account/components/login-form/use-otp-login-form.ts @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InjectionToken } from '@angular/core'; + +export const USE_ONE_TIME_PASSWORD_LOGIN = new InjectionToken( + 'feature flag to enable enhanced UI related pages login', + { providedIn: 'root', factory: () => false } +); + +export const ONE_TIME_PASSWORD_LOGIN_PURPOSE = 'LOGIN'; diff --git a/feature-libs/user/account/core/connectors/converters.ts b/feature-libs/user/account/core/connectors/converters.ts index 33b343472f8..163a5c2c131 100644 --- a/feature-libs/user/account/core/connectors/converters.ts +++ b/feature-libs/user/account/core/connectors/converters.ts @@ -6,7 +6,7 @@ import { InjectionToken } from '@angular/core'; import { Converter } from '@spartacus/core'; -import { User } from '@spartacus/user/account/root'; +import { LoginForm, User, VerificationToken } from '@spartacus/user/account/root'; export const USER_ACCOUNT_NORMALIZER = new InjectionToken>( 'UserAccountNormalizer' @@ -15,3 +15,13 @@ export const USER_ACCOUNT_NORMALIZER = new InjectionToken>( export const USER_ACCOUNT_SERIALIZER = new InjectionToken>( 'UserAccountSerializer' ); + + +export const VERIFICATION_TOKEN_NORMALIZER = new InjectionToken>( + 'VerificationTokenNormalizer' +); + +export const LOGIN_FORM_SERIALIZER = new InjectionToken>( + 'LoginFormSerializer' +); + diff --git a/feature-libs/user/account/core/connectors/user-account.adapter.ts b/feature-libs/user/account/core/connectors/user-account.adapter.ts index 01fc77dab4a..153fc8bc804 100644 --- a/feature-libs/user/account/core/connectors/user-account.adapter.ts +++ b/feature-libs/user/account/core/connectors/user-account.adapter.ts @@ -4,9 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + LoginForm, + User, + VerificationToken, +} from '@spartacus/user/account/root'; import { Observable } from 'rxjs'; -import { User } from '@spartacus/user/account/root'; export abstract class UserAccountAdapter { abstract load(userId: string): Observable; + abstract createVerificationToken( + form: LoginForm + ): Observable; } diff --git a/feature-libs/user/account/core/connectors/user-account.connector.ts b/feature-libs/user/account/core/connectors/user-account.connector.ts index d2c5580da52..428414d4853 100644 --- a/feature-libs/user/account/core/connectors/user-account.connector.ts +++ b/feature-libs/user/account/core/connectors/user-account.connector.ts @@ -5,8 +5,12 @@ */ import { Injectable } from '@angular/core'; +import { + LoginForm, + User, + VerificationToken, +} from '@spartacus/user/account/root'; import { Observable } from 'rxjs'; -import { User } from '@spartacus/user/account/root'; import { UserAccountAdapter } from './user-account.adapter'; @Injectable() @@ -16,4 +20,7 @@ export class UserAccountConnector { get(userId: string): Observable { return this.adapter.load(userId); } + createVerificationToken(form: LoginForm): Observable { + return this.adapter.createVerificationToken(form); + } } diff --git a/feature-libs/user/account/core/facade/facade-providers.ts b/feature-libs/user/account/core/facade/facade-providers.ts index c6039c582dd..da7ba9f8c32 100644 --- a/feature-libs/user/account/core/facade/facade-providers.ts +++ b/feature-libs/user/account/core/facade/facade-providers.ts @@ -4,9 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { UserAccountService } from './user-account.service'; -import { UserAccountFacade } from '@spartacus/user/account/root'; import { Provider } from '@angular/core'; +import { + UserAccountFacade, + VerificationTokenFacade, +} from '@spartacus/user/account/root'; +import { UserAccountService } from './user-account.service'; +import { VerificationTokenService } from './verification-token.service'; export const facadeProviders: Provider[] = [ UserAccountService, @@ -14,4 +18,9 @@ export const facadeProviders: Provider[] = [ provide: UserAccountFacade, useExisting: UserAccountService, }, + VerificationTokenService, + { + provide: VerificationTokenFacade, + useExisting: VerificationTokenService, + }, ]; diff --git a/feature-libs/user/account/core/facade/index.ts b/feature-libs/user/account/core/facade/index.ts index ec5c6228586..eed2971eff1 100644 --- a/feature-libs/user/account/core/facade/index.ts +++ b/feature-libs/user/account/core/facade/index.ts @@ -5,3 +5,4 @@ */ export * from './user-account.service'; +export * from './verification-token.service'; diff --git a/feature-libs/user/account/core/facade/verification-token.service.ts b/feature-libs/user/account/core/facade/verification-token.service.ts new file mode 100644 index 00000000000..6238ae2ade9 --- /dev/null +++ b/feature-libs/user/account/core/facade/verification-token.service.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { Command, CommandService } from '@spartacus/core'; +import { Observable } from 'rxjs'; +import { VerificationTokenFacade } from '../../root/facade'; +import { LoginForm, VerificationToken } from '../../root/model'; +import { UserAccountConnector } from '../connectors'; + +@Injectable() +export class VerificationTokenService implements VerificationTokenFacade { + protected createVerificationTokenCommand: Command< + { form: LoginForm }, + VerificationToken + > = this.command.create(({ form }) => + this.connector.createVerificationToken(form) + ); + + constructor( + protected connector: UserAccountConnector, + protected command: CommandService + ) {} + + /** + * create verification token + */ + createVerificationToken(form: LoginForm): Observable { + return this.createVerificationTokenCommand.execute({ form }); + } +} diff --git a/feature-libs/user/account/occ/adapters/config/default-occ-user-account-endpoint.config.ts b/feature-libs/user/account/occ/adapters/config/default-occ-user-account-endpoint.config.ts index 52120c3a04f..d4340ec1665 100644 --- a/feature-libs/user/account/occ/adapters/config/default-occ-user-account-endpoint.config.ts +++ b/feature-libs/user/account/occ/adapters/config/default-occ-user-account-endpoint.config.ts @@ -8,6 +8,11 @@ import { OccConfig } from '@spartacus/core'; export const defaultOccUserAccountConfig: OccConfig = { backend: { - occ: { endpoints: { user: 'users/${userId}' } }, + occ: { + endpoints: { + user: 'users/${userId}', + createVerificationToken: 'users/anonymous/verificationToken', + }, + }, }, }; diff --git a/feature-libs/user/account/occ/adapters/config/occ-user-account-endpoint.model.ts b/feature-libs/user/account/occ/adapters/config/occ-user-account-endpoint.model.ts index cdd6bfbaf19..e04ae1cd358 100644 --- a/feature-libs/user/account/occ/adapters/config/occ-user-account-endpoint.model.ts +++ b/feature-libs/user/account/occ/adapters/config/occ-user-account-endpoint.model.ts @@ -11,6 +11,11 @@ export interface UserAccountOccEndpoints { * Get user account details */ user?: string | OccEndpoint; + + /** + * Create one time password for user login + */ + createVerificationToken?: string | OccEndpoint; } declare module '@spartacus/core' { interface OccEndpoints extends UserAccountOccEndpoints {} diff --git a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.ts b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.ts index 494284031d2..7971877823c 100644 --- a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.ts +++ b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.ts @@ -4,23 +4,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; import { ConverterService, + InterceptorUtil, LoggerService, - normalizeHttpError, Occ, OccEndpointsService, + USE_CLIENT_TOKEN, + normalizeHttpError, } from '@spartacus/core'; import { + LOGIN_FORM_SERIALIZER, USER_ACCOUNT_NORMALIZER, UserAccountAdapter, + VERIFICATION_TOKEN_NORMALIZER, } from '@spartacus/user/account/core'; -import { User } from '@spartacus/user/account/root'; +import { + LoginForm, + User, + VerificationToken, +} from '@spartacus/user/account/root'; import { Observable } from 'rxjs'; import { catchError } from 'rxjs/operators'; +const CONTENT_TYPE_JSON_HEADER = { 'Content-Type': 'application/json' }; + @Injectable() export class OccUserAccountAdapter implements UserAccountAdapter { protected logger = inject(LoggerService); @@ -40,4 +50,24 @@ export class OccUserAccountAdapter implements UserAccountAdapter { this.converter.pipeable(USER_ACCOUNT_NORMALIZER) ); } + + createVerificationToken(form: LoginForm): Observable { + const url = this.occEndpoints.buildUrl('createVerificationToken'); + + const headers = InterceptorUtil.createHeader( + USE_CLIENT_TOKEN, + true, + new HttpHeaders({ + ...CONTENT_TYPE_JSON_HEADER, + }) + ); + form = this.converter.convert(form, LOGIN_FORM_SERIALIZER); + + return this.http.post(url, form, { headers }).pipe( + catchError((error) => { + throw normalizeHttpError(error, this.logger); + }), + this.converter.pipeable(VERIFICATION_TOKEN_NORMALIZER) + ); + } } diff --git a/feature-libs/user/account/root/facade/index.ts b/feature-libs/user/account/root/facade/index.ts index 52276c66cfd..fa1ec5e5a4d 100644 --- a/feature-libs/user/account/root/facade/index.ts +++ b/feature-libs/user/account/root/facade/index.ts @@ -5,3 +5,4 @@ */ export * from './user-account.facade'; +export * from './verification-token.facade'; diff --git a/feature-libs/user/account/root/facade/verification-token.facade.ts b/feature-libs/user/account/root/facade/verification-token.facade.ts new file mode 100644 index 00000000000..bae1c7fa3ce --- /dev/null +++ b/feature-libs/user/account/root/facade/verification-token.facade.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { facadeFactory } from '@spartacus/core'; +import { Observable } from 'rxjs'; +import { USER_ACCOUNT_CORE_FEATURE } from '../feature-name'; +import { LoginForm, VerificationToken } from '../model'; + +@Injectable({ + providedIn: 'root', + useFactory: () => + facadeFactory({ + facade: VerificationTokenFacade, + feature: USER_ACCOUNT_CORE_FEATURE, + methods: ['createVerificationToken'], + }), +}) +export abstract class VerificationTokenFacade { + abstract createVerificationToken( + form: LoginForm + ): Observable; +} diff --git a/feature-libs/user/account/root/model/user.model.ts b/feature-libs/user/account/root/model/user.model.ts index 976fa7fe4f1..d89ba2eeb9a 100644 --- a/feature-libs/user/account/root/model/user.model.ts +++ b/feature-libs/user/account/root/model/user.model.ts @@ -13,3 +13,15 @@ export interface User { customerId?: string; title?: string; } + +export interface LoginForm { + purpose: string; + loginId: string; + password: string; +} + +export interface VerificationToken { + expiresIn: string; + tokenId: string; +} + diff --git a/feature-libs/user/account/styles/_index.scss b/feature-libs/user/account/styles/_index.scss index 0dff952259b..791101b695f 100644 --- a/feature-libs/user/account/styles/_index.scss +++ b/feature-libs/user/account/styles/_index.scss @@ -1,3 +1,4 @@ @import './login'; @import './login-form'; @import './my-account-v2-user'; +@import './otp-login-form'; diff --git a/feature-libs/user/account/styles/_otp-login-form.scss b/feature-libs/user/account/styles/_otp-login-form.scss new file mode 100644 index 00000000000..0ed3a2d68bd --- /dev/null +++ b/feature-libs/user/account/styles/_otp-login-form.scss @@ -0,0 +1,10 @@ +%cx-otp-login-form { + &.user-form { + cx-spinner { + display: none; + } + button { + flex: 100%; + } + } +} diff --git a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts index b948e9a0674..57d38ef58a9 100644 --- a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts +++ b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts @@ -28,6 +28,8 @@ import { USER_PROFILE_FEATURE, } from '@spartacus/user/profile/root'; import { environment } from '../../../../environments/environment'; +import { USE_ONE_TIME_PASSWORD_LOGIN } from '@spartacus/user/account/components'; + @NgModule({ imports: [UserAccountRootModule, UserProfileRootModule], @@ -71,6 +73,10 @@ import { environment } from '../../../../environments/environment'; provide: USE_MY_ACCOUNT_V2_PASSWORD, useValue: environment.myAccountV2, }, + { + provide: USE_ONE_TIME_PASSWORD_LOGIN, + useValue: environment.otpUserLogin, + }, provideConfig({ i18n: { resources: userProfileTranslations, diff --git a/projects/storefrontapp/src/environments/environment.prod.ts b/projects/storefrontapp/src/environments/environment.prod.ts index 73df861f676..7e6e81a28c2 100644 --- a/projects/storefrontapp/src/environments/environment.prod.ts +++ b/projects/storefrontapp/src/environments/environment.prod.ts @@ -22,4 +22,5 @@ export const environment: Environment = { requestedDeliveryDate: buildProcess.env.CX_REQUESTED_DELIVERY_DATE, pdfInvoices: buildProcess.env.CX_PDF_INVOICES, myAccountV2: buildProcess.env.CX_MY_ACCOUNT_V2 ?? false, + otpUserLogin: buildProcess.env.CX_OTP_USER_LOGIN ?? false, }; diff --git a/projects/storefrontapp/src/environments/environment.ts b/projects/storefrontapp/src/environments/environment.ts index 5ac06fae96e..182485edb5f 100644 --- a/projects/storefrontapp/src/environments/environment.ts +++ b/projects/storefrontapp/src/environments/environment.ts @@ -35,4 +35,5 @@ export const environment: Environment = { requestedDeliveryDate: buildProcess.env.CX_REQUESTED_DELIVERY_DATE ?? false, pdfInvoices: buildProcess.env.CX_PDF_INVOICES ?? false, myAccountV2: buildProcess.env.CX_MY_ACCOUNT_V2 ?? false, + otpUserLogin: buildProcess.env.CX_OTP_USER_LOGIN ?? false, }; diff --git a/projects/storefrontapp/src/environments/models/build.process.env.d.ts b/projects/storefrontapp/src/environments/models/build.process.env.d.ts index 3e52ae56530..6b4cf23c8f6 100644 --- a/projects/storefrontapp/src/environments/models/build.process.env.d.ts +++ b/projects/storefrontapp/src/environments/models/build.process.env.d.ts @@ -24,4 +24,5 @@ interface Env { CX_REQUESTED_DELIVERY_DATE: boolean; CX_PDF_INVOICES: boolean; CX_MY_ACCOUNT_V2: boolean; + CX_OTP_USER_LOGIN: boolean; } diff --git a/projects/storefrontapp/src/environments/models/environment.model.ts b/projects/storefrontapp/src/environments/models/environment.model.ts index 754f3f7d5c2..a84a39ed7c7 100644 --- a/projects/storefrontapp/src/environments/models/environment.model.ts +++ b/projects/storefrontapp/src/environments/models/environment.model.ts @@ -20,4 +20,5 @@ export interface Environment { requestedDeliveryDate: boolean; pdfInvoices: boolean; myAccountV2: boolean; + otpUserLogin: boolean; } From 7076eeb30cf0564b4421d369565f7c41bf74862f Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Fri, 12 Apr 2024 10:42:17 +0800 Subject: [PATCH 02/66] CXSPA-6689: add otp login component --- feature-libs/user/_index.scss | 6 +- .../assets/translations/en/userAccount.json | 14 ++- .../assets/translations/translations.ts | 2 +- .../components/otp-login-form/index.ts | 9 ++ .../otp-login-form-component.service.ts | 85 +++++++++++++++++++ .../otp-login-form.component.html | 59 +++++++++++++ .../otp-login-form.component.ts | 85 +++++++++++++++++++ .../otp-login-form/otp-login-form.module.ts | 62 ++++++++++++++ .../user-account-component.module.ts | 3 + .../account/root/user-account-root.module.ts | 1 + feature-libs/user/account/styles/_index.scss | 1 + .../user/account/styles/_otp-login-form.scss | 45 ++++++++++ .../removed-public-api-deprecation.ts | 3 +- .../4_0/rename-symbol/rename-symbol.ts | 7 ++ projects/schematics/src/shared/constants.ts | 1 + 15 files changed, 377 insertions(+), 6 deletions(-) create mode 100644 feature-libs/user/account/components/otp-login-form/index.ts create mode 100644 feature-libs/user/account/components/otp-login-form/otp-login-form-component.service.ts create mode 100644 feature-libs/user/account/components/otp-login-form/otp-login-form.component.html create mode 100644 feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts create mode 100644 feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts create mode 100644 feature-libs/user/account/styles/_otp-login-form.scss diff --git a/feature-libs/user/_index.scss b/feature-libs/user/_index.scss index e86984fedb2..8366c1fa8f1 100644 --- a/feature-libs/user/_index.scss +++ b/feature-libs/user/_index.scss @@ -6,9 +6,9 @@ $skipComponentStyles: () !default; $selectors: cx-address-book, cx-address-form, cx-suggested-addresses-dialog, - cx-login, cx-login-form, cx-register, cx-reset-password, cx-close-account, - cx-close-account-modal, cx-my-account-v2-profile, cx-my-account-v2-email, - cx-my-account-v2-password !default; + cx-login, cx-login-form, cx-otp-login-form, cx-register, cx-reset-password, + cx-close-account, cx-close-account-modal, cx-my-account-v2-profile, + cx-my-account-v2-email, cx-my-account-v2-password !default; @each $selector in $selectors { #{$selector} { diff --git a/feature-libs/user/account/assets/translations/en/userAccount.json b/feature-libs/user/account/assets/translations/en/userAccount.json index 9e9e12627ea..1505afa95db 100644 --- a/feature-libs/user/account/assets/translations/en/userAccount.json +++ b/feature-libs/user/account/assets/translations/en/userAccount.json @@ -15,6 +15,18 @@ }, "wrongEmailFormat": "This is not a valid email format." }, + "otpLoginForm": { + "sentOTP": "Verification code has been sent to {{target}}. Please enter the code.", + "sendRateLime": "in {{waitTime}}s", + "resend": "Resend", + "verificationCode": { + "label": "Verification Code", + "placeholder": "Enter Verification Code" + }, + "noReceiveCode": "Didn't receive the code?", + "verify": "Verify", + "back": "Back" + }, "miniLogin": { "userGreeting": "Hi, {{name}}", "signInRegister": "Sign In / Register" @@ -22,4 +34,4 @@ "myAccountV2User": { "signOut": "Sign Out" } -} +} \ No newline at end of file diff --git a/feature-libs/user/account/assets/translations/translations.ts b/feature-libs/user/account/assets/translations/translations.ts index fecdc65fe92..df9cd470cd3 100644 --- a/feature-libs/user/account/assets/translations/translations.ts +++ b/feature-libs/user/account/assets/translations/translations.ts @@ -12,5 +12,5 @@ export const userAccountTranslations: TranslationResources = { }; export const userAccountTranslationChunksConfig: TranslationChunksConfig = { - userAccount: ['loginForm', 'miniLogin', 'myAccountV2User'], + userAccount: ['loginForm', 'otpLoginForm', 'miniLogin', 'myAccountV2User'], }; diff --git a/feature-libs/user/account/components/otp-login-form/index.ts b/feature-libs/user/account/components/otp-login-form/index.ts new file mode 100644 index 00000000000..597303f60b4 --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './login-form.component'; +export * from './login-form.module'; +export * from './otp-login-form-component.service'; diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form-component.service.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form-component.service.ts new file mode 100644 index 00000000000..947017865ad --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form-component.service.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { + AuthService, + GlobalMessageService, + GlobalMessageType, + WindowRef, +} from '@spartacus/core'; +import { BehaviorSubject, from } from 'rxjs'; +import { tap, withLatestFrom } from 'rxjs/operators'; + +@Injectable() +export class OTPLoginFormComponentService { + constructor( + protected auth: AuthService, + protected globalMessage: GlobalMessageService, + protected winRef: WindowRef + ) {} + + protected busy$ = new BehaviorSubject(false); + + isUpdating$ = this.busy$.pipe( + tap((state) => { + state === true ? this.form.disable() : this.form.enable(); + }) + ); + + form: UntypedFormGroup = new UntypedFormGroup({ + tokenId: new UntypedFormControl('', [Validators.required]), + tokenCode: new UntypedFormControl('', Validators.required), + }); + + login() { + if (!this.form.valid) { + this.form.markAllAsTouched(); + return; + } + debugger; + this.busy$.next(true); + + from( + this.auth.loginWithCredentials( + this.form.value.tokenId, + this.form.value.tokenCode + ) + ) + .pipe( + withLatestFrom(this.auth.isUserLoggedIn()), + tap(([_, isLoggedIn]) => this.onSuccess(isLoggedIn)) + ) + .subscribe(); + } + + displayMessage(target: string) { + this.globalMessage.add( + { + key: 'otpLoginForm.sentOTP', + params: { target }, + }, + GlobalMessageType.MSG_TYPE_CONFIRMATION, + 10000 + ); + } + + protected onSuccess(isLoggedIn: boolean): void { + if (isLoggedIn) { + // We want to remove error messages on successful login (primary the bad + // username/password combination) + this.globalMessage.remove(GlobalMessageType.MSG_TYPE_ERROR); + this.form.reset(); + } + + this.busy$.next(false); + } +} diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.html b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.html new file mode 100644 index 00000000000..44174935b08 --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.html @@ -0,0 +1,59 @@ + + +
+ + + + + + +
+ + {{ 'otpLoginForm.back' | cxTranslate }} +
+
diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts new file mode 100644 index 00000000000..5f6750d4eea --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + HostBinding, +} from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { OTPLoginFormComponentService } from './otp-login-form-component.service'; + +@Component({ + selector: 'cx-otp-login-form', + templateUrl: './otp-login-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OTPLoginFormComponent { + constructor( + protected service: OTPLoginFormComponentService, + private route: ActivatedRoute, + private cdr: ChangeDetectorRef + ) {} + + form: UntypedFormGroup = this.service.form; + isUpdating$: Observable = this.service.isUpdating$; + + @HostBinding('class.user-form') style = true; + + tokenId: string; + + tokenCode: string; + + target: string; + + password: string; + + waitTime: int = 60; + + isResendDisabled: boolean = true; + + ngOnInit() { + this.route.params.subscribe((params) => { + this.tokenId = ''; + this.password = params['password']; + this.target = params['targetEmail']; + }); + this.tokenId = ''; + this.setWaitTime(); + this.service.displayMessage(this.target); + } + + onSubmit(): void { + debugger; + this.service.login(); + } + + resendOTP(): void { + this.isResendDisabled = true; + this.waitTime = 60; + this.service.displayMessage(this.target); + this.setWaitTime(); + } + + setWaitTime(): void { + let interval = setInterval(() => { + this.waitTime--; + this.cdr.detectChanges(); + if (this.waitTime <= 0) { + clearInterval(interval); + this.isResendDisabled = false; + this.cdr.detectChanges(); + } + }, 1000); + } + + openInfoDailog(): void { + throw new Error('Method not implemented.'); + } +} diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts new file mode 100644 index 00000000000..edc5dfc7460 --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { + AuthService, + CmsConfig, + FeaturesConfigModule, + GlobalMessageService, + I18nModule, + NotAuthGuard, + UrlModule, + WindowRef, + provideDefaultConfig, +} from '@spartacus/core'; +import { + FormErrorsModule, + PasswordVisibilityToggleModule, + SpinnerModule, +} from '@spartacus/storefront'; +import { OTPLoginFormComponentService } from './otp-login-form-component.service'; +import { OTPLoginFormComponent } from './otp-login-form.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule, + UrlModule, + I18nModule, + FormErrorsModule, + SpinnerModule, + PasswordVisibilityToggleModule, + FeaturesConfigModule, + ], + providers: [ + provideDefaultConfig({ + cmsComponents: { + ReturningCustomerOTPLoginComponent: { + component: OTPLoginFormComponent, + guards: [NotAuthGuard], + providers: [ + { + provide: OTPLoginFormComponentService, + useClass: OTPLoginFormComponentService, + deps: [AuthService, GlobalMessageService, WindowRef], + }, + ], + }, + }, + }), + ], + declarations: [OTPLoginFormComponent], +}) +export class OTPLoginFormModule {} diff --git a/feature-libs/user/account/components/user-account-component.module.ts b/feature-libs/user/account/components/user-account-component.module.ts index 660502c6d38..d91f292f68f 100644 --- a/feature-libs/user/account/components/user-account-component.module.ts +++ b/feature-libs/user/account/components/user-account-component.module.ts @@ -6,6 +6,8 @@ import { NgModule } from '@angular/core'; import { LoginFormModule } from './login-form/login-form.module'; +import { OTPLoginFormModule } from './otp-login-form/otp-login-form.module'; + import { LoginRegisterModule } from './login-register/login-register.module'; import { LoginModule } from './login/login.module'; import { MyAccountV2UserModule } from './my-account-v2-user'; @@ -14,6 +16,7 @@ import { MyAccountV2UserModule } from './my-account-v2-user'; imports: [ LoginModule, LoginFormModule, + OTPLoginFormModule, LoginRegisterModule, MyAccountV2UserModule, ], diff --git a/feature-libs/user/account/root/user-account-root.module.ts b/feature-libs/user/account/root/user-account-root.module.ts index f8b8c7f2d1f..b453d0c6f3a 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -20,6 +20,7 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { cmsComponents: [ 'LoginComponent', 'ReturningCustomerLoginComponent', + 'ReturningCustomerOTPLoginComponent', 'ReturningCustomerRegisterComponent', 'MyAccountViewUserComponent', ], diff --git a/feature-libs/user/account/styles/_index.scss b/feature-libs/user/account/styles/_index.scss index 0dff952259b..9ec88ff8fd0 100644 --- a/feature-libs/user/account/styles/_index.scss +++ b/feature-libs/user/account/styles/_index.scss @@ -1,3 +1,4 @@ @import './login'; @import './login-form'; +@import './otp-login-form'; @import './my-account-v2-user'; diff --git a/feature-libs/user/account/styles/_otp-login-form.scss b/feature-libs/user/account/styles/_otp-login-form.scss new file mode 100644 index 00000000000..2e6814bd771 --- /dev/null +++ b/feature-libs/user/account/styles/_otp-login-form.scss @@ -0,0 +1,45 @@ +%cx-otp-login-form { + &.user-form { + .resend-link-text { + width: 100%; + margin: auto; + + .left-text { + padding: 0; + text-align: left; + } + + .right-text { + padding: 0; + text-align: right; + } + + a.disabled-link { + pointer-events: none; + color: #a7cce8; + text-decoration: none; + } + + a { + font-size: 16px; + font-weight: 400; + color: #1672b7; + white-space: nowrap; + text-decoration: none; + } + } + + .verify-container { + width: 100%; + margin-top: 40px; + } + + cx-spinner { + display: none; + } + + button { + flex: 100%; + } + } +} diff --git a/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts b/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts index 63a607f8c3d..fdfababa283 100644 --- a/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts +++ b/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts @@ -47,6 +47,7 @@ import { OCC_USER_ACCOUNT_ADAPTER, OCC_USER_ADAPTER, OCC_USER_PROFILE_ADAPTER, + OTP_LOGIN_FORM_MODULE, PAGE_EVENT_BUILDER, PAGE_EVENT_MODULE, PRODUCT_VARIANTS_MODULE, @@ -369,7 +370,7 @@ export const REMOVED_PUBLIC_API_DATA: DeprecatedNode[] = [ { node: USER_COMPONENT_MODULE, importPath: SPARTACUS_STOREFRONTLIB, - comment: `'${USER_COMPONENT_MODULE}' - Following module imports '${LOGIN_MODULE}', '${LOGIN_FORM_MODULE}', '${LOGIN_REGISTER_MODULE}', '${REGISTER_COMPONENT_MODULE}' were removed. Those modules are now part of ${SPARTACUS_USER}.`, + comment: `'${USER_COMPONENT_MODULE}' - Following module imports '${LOGIN_MODULE}', '${LOGIN_FORM_MODULE}','${OTP_LOGIN_FORM_MODULE}', '${LOGIN_REGISTER_MODULE}', '${REGISTER_COMPONENT_MODULE}' were removed. Those modules are now part of ${SPARTACUS_USER}.`, }, // projects/storefrontlib/cms-components/myaccount/close-account/components/close-account-modal/close-account-modal.component.ts { diff --git a/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts b/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts index a0e599ff716..01a1f5a5266 100644 --- a/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts +++ b/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts @@ -44,6 +44,7 @@ import { LOGIN_REGISTER_MODULE, OCC_ASM_ADAPTER, ORDER_ENTRY, + OTP_LOGIN_FORM_MODULE, PERMISSION_ROUTING_CONFIG, PERSONALIZATION_ACTION, PERSONALIZATION_CONFIG, @@ -418,6 +419,12 @@ export const RENAMED_SYMBOLS_DATA: RenamedSymbol[] = [ previousImportPath: SPARTACUS_STOREFRONTLIB, newImportPath: SPARTACUS_USER_ACCOUNT_COMPONENTS, }, + // projects/storefrontlib/cms-components/user/otp-login-form/otp-login-form.module.ts + { + previousNode: OTP_LOGIN_FORM_MODULE, + previousImportPath: SPARTACUS_STOREFRONTLIB, + newImportPath: SPARTACUS_USER_ACCOUNT_COMPONENTS, + }, // projects/storefrontlib/cms-components/user/login-register/login-register.module.ts { previousNode: LOGIN_REGISTER_MODULE, diff --git a/projects/schematics/src/shared/constants.ts b/projects/schematics/src/shared/constants.ts index 884ac911024..1007bf0b9d8 100644 --- a/projects/schematics/src/shared/constants.ts +++ b/projects/schematics/src/shared/constants.ts @@ -864,6 +864,7 @@ export const RESET_PASSWORD_FORM_COMPONENT = 'ResetPasswordFormComponent'; export const LOGIN_COMPONENT = 'LoginComponent'; export const LOGIN_MODULE = 'LoginModule'; export const LOGIN_FORM_MODULE = 'LoginFormModule'; +export const OTP_LOGIN_FORM_MODULE = 'OTPLoginFormModule'; export const LOGIN_REGISTER_COMPONENT = 'LoginRegisterComponent'; export const LOGIN_REGISTER_MODULE = 'LoginRegisterModule'; From 1d99f57c59a48ee74425ea80107c6d2027386085 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Fri, 12 Apr 2024 14:05:54 +0800 Subject: [PATCH 03/66] CXSPA-6689: code refined --- feature-libs/user/account/components/login-form/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/feature-libs/user/account/components/login-form/index.ts b/feature-libs/user/account/components/login-form/index.ts index 81181c5629d..73b08aa59d2 100644 --- a/feature-libs/user/account/components/login-form/index.ts +++ b/feature-libs/user/account/components/login-form/index.ts @@ -7,3 +7,5 @@ export * from './login-form-component.service'; export * from './login-form.component'; export * from './login-form.module'; +export * from './otp-login-form.component'; +export * from './use-otp-login-form'; From 24bff84b792aba5a895ec1ccd82bdfc198401a76 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Fri, 12 Apr 2024 14:51:48 +0800 Subject: [PATCH 04/66] refine code --- .../user/account/components/login-form/index.ts | 2 ++ .../login-form/otp-login-form.component.ts | 17 ++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/feature-libs/user/account/components/login-form/index.ts b/feature-libs/user/account/components/login-form/index.ts index 81181c5629d..73b08aa59d2 100644 --- a/feature-libs/user/account/components/login-form/index.ts +++ b/feature-libs/user/account/components/login-form/index.ts @@ -7,3 +7,5 @@ export * from './login-form-component.service'; export * from './login-form.component'; export * from './login-form.module'; +export * from './otp-login-form.component'; +export * from './use-otp-login-form'; diff --git a/feature-libs/user/account/components/login-form/otp-login-form.component.ts b/feature-libs/user/account/components/login-form/otp-login-form.component.ts index b98fb4b6cba..a1ef08faed2 100644 --- a/feature-libs/user/account/components/login-form/otp-login-form.component.ts +++ b/feature-libs/user/account/components/login-form/otp-login-form.component.ts @@ -4,18 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - ChangeDetectionStrategy, - Component, - HostBinding, - inject, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; import { UntypedFormControl, UntypedFormGroup, Validators, } from '@angular/forms'; -import { WindowRef } from '@spartacus/core'; +import { RoutingService, WindowRef } from '@spartacus/core'; import { CustomFormValidators } from '@spartacus/storefront'; import { BehaviorSubject, Observable, tap } from 'rxjs'; import { VerificationTokenFacade } from '../../root/facade/verification-token.facade'; @@ -28,8 +23,11 @@ import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from './use-otp-login-form'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class OneTimePasswordLoginFormComponent { - protected verificationTokenFacade = inject(VerificationTokenFacade); - protected winRef = inject(WindowRef); + constructor( + protected routingService: RoutingService, + protected verificationTokenFacade: VerificationTokenFacade, + protected winRef: WindowRef + ) {} protected busy$ = new BehaviorSubject(false); @@ -63,6 +61,7 @@ export class OneTimePasswordLoginFormComponent { this.verificationTokenFacade.createVerificationToken( this.collectDataFromLoginForm() ); + this.routingService.go({ cxRoute: 'login/otp' }); this.busy$.next(false); } From e3f78a62588a67f91fcb25809220c7694e4244bb Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Fri, 12 Apr 2024 16:04:16 +0800 Subject: [PATCH 05/66] CXSPA-6689 --- ...erification-token-form-component.service.ts | 11 ++++++++++- .../verification-token-form.component.ts | 18 ++++++++++-------- .../verification-token-form.module.ts | 2 +- .../account/root/user-account-root.module.ts | 2 +- .../routing/default-routing-config.ts | 5 +++++ 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts index f8bf0c1d657..6b45c39ef65 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts @@ -18,12 +18,14 @@ import { } from '@spartacus/core'; import { BehaviorSubject, from } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; +import { VerificationTokenFacade } from '../../root/facade'; @Injectable() export class VerificationTokenFormComponentService { constructor( protected auth: AuthService, protected globalMessage: GlobalMessageService, + protected verificationTokenFacade: VerificationTokenFacade, protected winRef: WindowRef ) {} @@ -45,7 +47,6 @@ export class VerificationTokenFormComponentService { this.form.markAllAsTouched(); return; } - debugger; this.busy$.next(true); from( @@ -72,6 +73,14 @@ export class VerificationTokenFormComponentService { ); } + sentOTP(loginId: string, password: string, purpose: string) { + this.verificationTokenFacade.createVerificationToken({ + loginId, + password, + purpose, + }); + } + protected onSuccess(isLoggedIn: boolean): void { if (isLoggedIn) { // We want to remove error messages on successful login (primary the bad diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index 5c6eeda3eda..19a231bb2be 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -40,34 +40,36 @@ export class VerificationTokenFormComponent { password: string; + purpose: string; + waitTime: int = 60; isResendDisabled: boolean = true; ngOnInit() { this.route.params.subscribe((params) => { - this.tokenId = ''; + this.tokenId = params['tokenId']; this.password = params['password']; - this.target = params['targetEmail']; + this.target = params['loginId']; + this.purpose = params['purpose']; + this.service.displayMessage(this.target); }); - this.tokenId = ''; - this.setWaitTime(); - this.service.displayMessage(this.target); + this.startWaitTimeInterval(); } onSubmit(): void { - debugger; this.service.login(); } resendOTP(): void { this.isResendDisabled = true; this.waitTime = 60; + this.startWaitTimeInterval(); + this.service.sentOTP(this.target, this.password, this.purpose); this.service.displayMessage(this.target); - this.setWaitTime(); } - setWaitTime(): void { + startWaitTimeInterval(): void { let interval = setInterval(() => { this.waitTime--; this.cdr.detectChanges(); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts index a5d1dbcc0a3..2b5a90a40b2 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts @@ -43,7 +43,7 @@ import { VerificationTokenFormComponent } from './verification-token-form.compon providers: [ provideDefaultConfig({ cmsComponents: { - ReturningCustomerOTPLoginComponent: { + VerifyOTPTokenComponent: { component: VerificationTokenFormComponent, guards: [NotAuthGuard], providers: [ diff --git a/feature-libs/user/account/root/user-account-root.module.ts b/feature-libs/user/account/root/user-account-root.module.ts index b453d0c6f3a..ae5983064a5 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -20,7 +20,7 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { cmsComponents: [ 'LoginComponent', 'ReturningCustomerLoginComponent', - 'ReturningCustomerOTPLoginComponent', + 'VerifyOTPTokenComponent', 'ReturningCustomerRegisterComponent', 'MyAccountViewUserComponent', ], diff --git a/projects/storefrontlib/cms-structure/routing/default-routing-config.ts b/projects/storefrontlib/cms-structure/routing/default-routing-config.ts index 46dae0f8595..106a381e3e2 100644 --- a/projects/storefrontlib/cms-structure/routing/default-routing-config.ts +++ b/projects/storefrontlib/cms-structure/routing/default-routing-config.ts @@ -16,6 +16,11 @@ export const defaultStorefrontRoutesConfig: RoutesConfig = { protected: false, authFlow: true, }, + verifyToken: { + paths: ['/login/verify-token'], + protected: false, + authFlow: true, + }, register: { paths: ['login/register'], protected: false, From 8b4685492faf376f31984d145685cf53c669ba1f Mon Sep 17 00:00:00 2001 From: niehuayang Date: Fri, 12 Apr 2024 18:43:02 +0800 Subject: [PATCH 06/66] test --- .../account/root/user-account-root.module.ts | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/feature-libs/user/account/root/user-account-root.module.ts b/feature-libs/user/account/root/user-account-root.module.ts index b453d0c6f3a..ce36cd0ef5b 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -5,7 +5,16 @@ */ import { NgModule } from '@angular/core'; -import { CmsConfig, provideDefaultConfigFactory } from '@spartacus/core'; +import { RouterModule } from '@angular/router'; +import { + AuthGuard, + CmsConfig, + RoutingConfig, + provideDefaultConfig, + provideDefaultConfigFactory, +} from '@spartacus/core'; +import { CmsPageGuard } from '@spartacus/storefront'; +import { VerificationTokenFormComponent } from '../components/verification-token-form'; import { UserAccountEventModule } from './events/user-account-event.module'; import { USER_ACCOUNT_CORE_FEATURE, @@ -33,7 +42,35 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { } @NgModule({ - imports: [UserAccountEventModule], - providers: [provideDefaultConfigFactory(defaultUserAccountComponentsConfig)], + imports: [ + UserAccountEventModule, + RouterModule.forChild([ + { + // @ts-ignore + path: null, + canActivate: [AuthGuard, CmsPageGuard], + component: VerificationTokenFormComponent, + data: { + cxRoute: 'loginByVerifyToken', + // cxContext: { + // [ORDER_ENTRIES_CONTEXT]: SavedCartOrderEntriesContextToken, + // }, + }, + }, + ]), + ], + providers: [ + provideDefaultConfigFactory(defaultUserAccountComponentsConfig), + provideDefaultConfig({ + routing: { + routes: { + loginByVerifyToken: { + paths: ['login/verify-token'], + paramsMapping: { savedCartId: 'savedCartId' }, + }, + }, + }, + }), + ], }) export class UserAccountRootModule {} From 780f91228bec5e41d1370edaeb7565b7b1bf424d Mon Sep 17 00:00:00 2001 From: niehuayang Date: Mon, 15 Apr 2024 16:07:44 +0800 Subject: [PATCH 07/66] add route --- .../login-form/otp-login-form.component.ts | 59 ++++++++++++++--- .../account/root/user-account-root.module.ts | 63 +++++++++---------- 2 files changed, 80 insertions(+), 42 deletions(-) diff --git a/feature-libs/user/account/components/login-form/otp-login-form.component.ts b/feature-libs/user/account/components/login-form/otp-login-form.component.ts index 01ace3a7d4b..2d2ea34ab57 100644 --- a/feature-libs/user/account/components/login-form/otp-login-form.component.ts +++ b/feature-libs/user/account/components/login-form/otp-login-form.component.ts @@ -10,11 +10,18 @@ import { UntypedFormGroup, Validators, } from '@angular/forms'; -import { RoutingService, WindowRef } from '@spartacus/core'; +import { + GlobalMessageService, + GlobalMessageType, + HttpErrorModel, + RoutingService, + TranslationService, + WindowRef, +} from '@spartacus/core'; import { CustomFormValidators } from '@spartacus/storefront'; import { BehaviorSubject, Observable, tap } from 'rxjs'; import { VerificationTokenFacade } from '../../root/facade/verification-token.facade'; -import { LoginForm } from '../../root/model'; +import { LoginForm, VerificationToken } from '../../root/model'; import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from './use-otp-login-form'; @Component({ @@ -26,7 +33,9 @@ export class OneTimePasswordLoginFormComponent { constructor( protected routingService: RoutingService, protected verificationTokenFacade: VerificationTokenFacade, - protected winRef: WindowRef + protected winRef: WindowRef, + protected translationService: TranslationService, + protected globalMessage: GlobalMessageService ) {} protected busy$ = new BehaviorSubject(false); @@ -58,14 +67,48 @@ export class OneTimePasswordLoginFormComponent { } this.busy$.next(true); - this.verificationTokenFacade.createVerificationToken( - this.collectDataFromLoginForm() - ); - this.routingService.go({ cxRoute: 'login/otp' }); + const loginForm = this.collectDataFromLoginForm(); + this.verificationTokenFacade.createVerificationToken(loginForm).subscribe({ + next: (result: VerificationToken) => + this.goToVerificationTokenForm(result, loginForm), + error: (error: HttpErrorModel) => + this.onCreateVerificationTokenFail(error), + complete: () => this.busy$.next(false), + }); + } + + protected onCreateVerificationTokenFail(error: HttpErrorModel): void { this.busy$.next(false); + const errorDetails = error.details ?? []; + if (errorDetails.length === 0) { + this.globalMessage.add( + { key: 'httpHandlers.unknownError' }, + GlobalMessageType.MSG_TYPE_ERROR + ); + } + errorDetails.forEach((err) => { + this.globalMessage.add( + { raw: err.message }, + GlobalMessageType.MSG_TYPE_ERROR + ); + }); + } + + protected goToVerificationTokenForm( + verificationToken: VerificationToken, + loginForm: LoginForm + ): void { + this.routingService.go({ + cxRoute: 'verifyToken', + params: { + loginId: loginForm.loginId, + password: loginForm.password, + tokenId: verificationToken.tokenId, + }, + }); } - collectDataFromLoginForm(): LoginForm { + protected collectDataFromLoginForm(): LoginForm { return { // TODO: consider dropping toLowerCase as this should not be part of the UI, // as it's too opinionated and doesn't work with other AUTH services diff --git a/feature-libs/user/account/root/user-account-root.module.ts b/feature-libs/user/account/root/user-account-root.module.ts index 4be1443024b..ff53708d80d 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -5,16 +5,7 @@ */ import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { - AuthGuard, - CmsConfig, - RoutingConfig, - provideDefaultConfig, - provideDefaultConfigFactory, -} from '@spartacus/core'; -import { CmsPageGuard } from '@spartacus/storefront'; -import { VerificationTokenFormComponent } from '../components/verification-token-form'; +import { CmsConfig, provideDefaultConfigFactory } from '@spartacus/core'; import { UserAccountEventModule } from './events/user-account-event.module'; import { USER_ACCOUNT_CORE_FEATURE, @@ -44,33 +35,37 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { @NgModule({ imports: [ UserAccountEventModule, - RouterModule.forChild([ - { - // @ts-ignore - path: null, - canActivate: [AuthGuard, CmsPageGuard], - component: VerificationTokenFormComponent, - data: { - cxRoute: 'loginByVerifyToken', - // cxContext: { - // [ORDER_ENTRIES_CONTEXT]: SavedCartOrderEntriesContextToken, - // }, - }, - }, - ]), + // RouterModule.forChild([ + // { + // // @ts-ignore + // path: null, + // canActivate: [AuthGuard, CmsPageGuard], + // component: PageLayoutComponent, + // data: { + // cxRoute: 'loginByVerifyToken', + // // cxContext: { + // // [ORDER_ENTRIES_CONTEXT]: SavedCartOrderEntriesContextToken, + // // }, + // }, + // }, + // ]), ], providers: [ provideDefaultConfigFactory(defaultUserAccountComponentsConfig), - provideDefaultConfig({ - routing: { - routes: { - loginByVerifyToken: { - paths: ['login/verify-token'], - paramsMapping: { savedCartId: 'savedCartId' }, - }, - }, - }, - }), + // provideDefaultConfig({ + // routing: { + // routes: { + // loginByVerifyToken: { + // paths: ['login/verify-token'], + // paramsMapping: { + // loginId: 'loginId', + // password: 'password', + // tokenId: 'tokenId', + // }, + // }, + // }, + // }, + // }), ], }) export class UserAccountRootModule {} From e27548007e5d836067342a1c5256d577c871d9d3 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Tue, 16 Apr 2024 18:08:28 +0800 Subject: [PATCH 08/66] CXSPA-6689: add dialog for otp token --- feature-libs/user/_index.scss | 8 +-- .../assets/translations/en/userAccount.json | 8 +++ .../assets/translations/translations.ts | 1 + ...efault-verification-token-layout.config.ts | 22 +++++++ .../verification-token-dialog.component.html | 52 ++++++++++++++++ .../verification-token-dialog.component.ts | 35 +++++++++++ ...rification-token-form-component.service.ts | 2 +- .../verification-token-form.component.html | 2 +- .../verification-token-form.component.ts | 13 +++- .../verification-token-form.module.ts | 14 ++++- .../root/model/augmented-core.model.ts | 13 ++++ feature-libs/user/account/root/model/index.ts | 1 + feature-libs/user/account/styles/_index.scss | 1 + .../styles/_verification-token-dialog.scss | 62 +++++++++++++++++++ .../styles/_verification-token-form.scss | 4 +- 15 files changed, 225 insertions(+), 13 deletions(-) create mode 100644 feature-libs/user/account/components/verification-token-form/default-verification-token-layout.config.ts create mode 100644 feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html create mode 100644 feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts create mode 100644 feature-libs/user/account/root/model/augmented-core.model.ts create mode 100644 feature-libs/user/account/styles/_verification-token-dialog.scss diff --git a/feature-libs/user/_index.scss b/feature-libs/user/_index.scss index fea387be8e1..9a7f20ff777 100644 --- a/feature-libs/user/_index.scss +++ b/feature-libs/user/_index.scss @@ -6,10 +6,10 @@ $skipComponentStyles: () !default; $selectors: cx-address-book, cx-address-form, cx-suggested-addresses-dialog, - cx-login, cx-login-form, cx-verification-token-form, cx-register, - cx-reset-password, cx-close-account, cx-close-account-modal, - cx-my-account-v2-profile, cx-my-account-v2-email, cx-my-account-v2-password, - cx-verification-token-form !default; + cx-login, cx-login-form, cx-register, cx-reset-password, cx-close-account, + cx-close-account-modal, cx-my-account-v2-profile, cx-my-account-v2-email, + cx-my-account-v2-password, cx-verification-token-form, + cx-verification-token-dialog !default; @each $selector in $selectors { #{$selector} { diff --git a/feature-libs/user/account/assets/translations/en/userAccount.json b/feature-libs/user/account/assets/translations/en/userAccount.json index efb995ba868..e109c12a4c0 100644 --- a/feature-libs/user/account/assets/translations/en/userAccount.json +++ b/feature-libs/user/account/assets/translations/en/userAccount.json @@ -27,6 +27,14 @@ "verify": "Verify", "back": "Back" }, + "verificationTokenDialog": { + "title": "Don't receive the code", + "noReceiveCode": "If you have not received the code, please try:", + "contentLine1": "1. Wait for a while as the email may be delayed.", + "contentLine2": "2. Check if it is in the junk mail.", + "contentLine3": "3. Go back and make sure your email address and password is correct.", + "ok": "ok" + }, "miniLogin": { "userGreeting": "Hi, {{name}}", "signInRegister": "Sign In / Register" diff --git a/feature-libs/user/account/assets/translations/translations.ts b/feature-libs/user/account/assets/translations/translations.ts index 7afe2651019..a4c1ca04cd6 100644 --- a/feature-libs/user/account/assets/translations/translations.ts +++ b/feature-libs/user/account/assets/translations/translations.ts @@ -15,6 +15,7 @@ export const userAccountTranslationChunksConfig: TranslationChunksConfig = { userAccount: [ 'loginForm', 'verificationTokenForm', + 'verificationTokenDialog', 'miniLogin', 'myAccountV2User', ], diff --git a/feature-libs/user/account/components/verification-token-form/default-verification-token-layout.config.ts b/feature-libs/user/account/components/verification-token-form/default-verification-token-layout.config.ts new file mode 100644 index 00000000000..9c6079c35ff --- /dev/null +++ b/feature-libs/user/account/components/verification-token-form/default-verification-token-layout.config.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + DIALOG_TYPE, + LAUNCH_CALLER, + LayoutConfig, +} from '@spartacus/storefront'; +import { VerificationTokenDialogComponent } from './verification-token-dialog.component'; + +export const defaultVerificationTokenLayoutConfig: LayoutConfig = { + launch: { + [LAUNCH_CALLER.ACCOUNT_VERIFICATION_TOKEN]: { + inlineRoot: true, + component: VerificationTokenDialogComponent, + dialogType: DIALOG_TYPE.DIALOG, + }, + }, +}; diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html new file mode 100644 index 00000000000..6888e267c40 --- /dev/null +++ b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html @@ -0,0 +1,52 @@ +
+
+ +
+ + + + + + + + +
+
+
diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts new file mode 100644 index 00000000000..55b98f67545 --- /dev/null +++ b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, OnInit } from '@angular/core'; +import { FocusConfig, LaunchDialogService } from '@spartacus/storefront'; + +export enum VERIFICATION_TOKEN_DIALOG_ACTION { + OK = 'OK', +} + +@Component({ + selector: 'cx-verification-token-dialog', + templateUrl: './verification-token-dialog.component.html', +}) +export class VerificationTokenDialogComponent implements OnInit { + VERIFICATION_TOKEN_DIALOG_ACTION = VERIFICATION_TOKEN_DIALOG_ACTION; + + focusConfig: FocusConfig = { + trap: true, + block: true, + autofocus: true, + focusOnEscape: true, + }; + + constructor(protected launchDialogService: LaunchDialogService) {} + + ngOnInit(): void {} + + closeModal(reason: VERIFICATION_TOKEN_DIALOG_ACTION): void { + this.launchDialogService.closeDialog(reason); + } +} diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts index 6b45c39ef65..c8d76d8cbfe 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts @@ -39,7 +39,7 @@ export class VerificationTokenFormComponentService { form: UntypedFormGroup = new UntypedFormGroup({ tokenId: new UntypedFormControl('', [Validators.required]), - tokenCode: new UntypedFormControl('', Validators.required), + tokenCode: new UntypedFormControl('', [Validators.required]), }); login() { diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html index 18341164fdf..080e1a5723f 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html @@ -37,7 +37,7 @@ diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index 19a231bb2be..3c40d60cc17 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -8,10 +8,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + ElementRef, HostBinding, + ViewChild, } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; import { Observable } from 'rxjs'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; @@ -23,6 +26,7 @@ import { VerificationTokenFormComponentService } from './verification-token-form export class VerificationTokenFormComponent { constructor( protected service: VerificationTokenFormComponentService, + protected launchDialogService: LaunchDialogService, private route: ActivatedRoute, private cdr: ChangeDetectorRef ) {} @@ -32,6 +36,8 @@ export class VerificationTokenFormComponent { @HostBinding('class.user-form') style = true; + @ViewChild('noReceiveCodeLink') element: ElementRef; + tokenId: string; tokenCode: string; @@ -48,7 +54,7 @@ export class VerificationTokenFormComponent { ngOnInit() { this.route.params.subscribe((params) => { - this.tokenId = params['tokenId']; + this.tokenId = 'test'; this.password = params['password']; this.target = params['loginId']; this.purpose = params['purpose']; @@ -82,6 +88,9 @@ export class VerificationTokenFormComponent { } openInfoDailog(): void { - throw new Error('Method not implemented.'); + this.launchDialogService.openDialogAndSubscribe( + LAUNCH_CALLER.ACCOUNT_VERIFICATION_TOKEN, + this.element + ); } } diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts index 2b5a90a40b2..f97053944a5 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts @@ -21,9 +21,12 @@ import { } from '@spartacus/core'; import { FormErrorsModule, - PasswordVisibilityToggleModule, + IconModule, + KeyboardFocusModule, SpinnerModule, } from '@spartacus/storefront'; +import { defaultVerificationTokenLayoutConfig } from './default-verification-token-layout.config'; +import { VerificationTokenDialogComponent } from './verification-token-dialog.component'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; import { VerificationTokenFormComponent } from './verification-token-form.component'; @@ -31,13 +34,14 @@ import { VerificationTokenFormComponent } from './verification-token-form.compon imports: [ CommonModule, FormsModule, + KeyboardFocusModule, ReactiveFormsModule, RouterModule, UrlModule, + IconModule, I18nModule, FormErrorsModule, SpinnerModule, - PasswordVisibilityToggleModule, FeaturesConfigModule, ], providers: [ @@ -56,7 +60,11 @@ import { VerificationTokenFormComponent } from './verification-token-form.compon }, }, }), + provideDefaultConfig(defaultVerificationTokenLayoutConfig), + ], + declarations: [ + VerificationTokenFormComponent, + VerificationTokenDialogComponent, ], - declarations: [VerificationTokenFormComponent], }) export class VerificationTokenFormModule {} diff --git a/feature-libs/user/account/root/model/augmented-core.model.ts b/feature-libs/user/account/root/model/augmented-core.model.ts new file mode 100644 index 00000000000..986f7b167b9 --- /dev/null +++ b/feature-libs/user/account/root/model/augmented-core.model.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import '@spartacus/storefront'; + +declare module '@spartacus/storefront' { + const enum LAUNCH_CALLER { + ACCOUNT_VERIFICATION_TOKEN = 'ACCOUNT_VERIFICATION_TOKEN', + } +} diff --git a/feature-libs/user/account/root/model/index.ts b/feature-libs/user/account/root/model/index.ts index 2fad56d332a..5b89674523c 100644 --- a/feature-libs/user/account/root/model/index.ts +++ b/feature-libs/user/account/root/model/index.ts @@ -4,4 +4,5 @@ * SPDX-License-Identifier: Apache-2.0 */ +export * from './augmented-core.model'; export * from './user.model'; diff --git a/feature-libs/user/account/styles/_index.scss b/feature-libs/user/account/styles/_index.scss index 7397482c473..6adb0b7c7a6 100644 --- a/feature-libs/user/account/styles/_index.scss +++ b/feature-libs/user/account/styles/_index.scss @@ -4,3 +4,4 @@ @import './my-account-v2-user'; @import './otp-login-form'; @import './verification-token-form'; +@import './verification-token-dialog'; diff --git a/feature-libs/user/account/styles/_verification-token-dialog.scss b/feature-libs/user/account/styles/_verification-token-dialog.scss new file mode 100644 index 00000000000..1f2dd70295d --- /dev/null +++ b/feature-libs/user/account/styles/_verification-token-dialog.scss @@ -0,0 +1,62 @@ +%cx-verification-token-dialog { + .cx-modal-content { + max-width: 32rem; + margin-inline-start: auto; + margin-inline-end: auto; + } + + .cx-dialog-header { + padding-top: 0.9rem; + padding-inline-end: 1rem; + padding-bottom: 0.9rem; + padding-inline-start: 1rem; + border-width: 0; + display: flex; + + box-shadow: 0px 0px 4px rgba(85, 107, 130, 0.16), inset 0px -1px 0px #d9d9d9; + + .info-icon { + cx-icon { + font-size: 1.4rem; + color: #0084fe; + } + } + + .title { + font-size: 1rem; + font-weight: 600; + } + } + + .cx-dialog-body { + padding: 1rem; + + .cx-dialog-row { + margin: 0; + display: flex; + padding-top: 0; + padding-inline-end: 0.875rem; + padding-bottom: 0.85rem; + padding-inline-start: 2.875rem; + + max-width: 100%; + flex-wrap: wrap; + + @include media-breakpoint-down(sm) { + flex-direction: column; + padding: 0; + } + } + + .cx-dialog-item { + padding: 0.2rem; + } + } + + .cx-dialog-footer { + padding-top: 0; + padding-inline-end: 1rem; + padding-bottom: 0; + padding-inline-start: 1rem; + } +} diff --git a/feature-libs/user/account/styles/_verification-token-form.scss b/feature-libs/user/account/styles/_verification-token-form.scss index 43dc111e0e3..294067da99d 100644 --- a/feature-libs/user/account/styles/_verification-token-form.scss +++ b/feature-libs/user/account/styles/_verification-token-form.scss @@ -21,7 +21,7 @@ } a { - font-size: 16px; + font-size: 1rem; font-weight: 400; color: #1672b7; white-space: nowrap; @@ -31,7 +31,7 @@ .verify-container { width: 100%; - margin-top: 40px; + margin-top: 2.5rem; } cx-spinner { From b35b343756ddd22bd13ad331ad3f85edfb70e52c Mon Sep 17 00:00:00 2001 From: niehuayang Date: Wed, 17 Apr 2024 11:04:28 +0800 Subject: [PATCH 09/66] refine code --- feature-libs/user/_index.scss | 2 +- .../login-form/otp-login-form.component.ts | 22 ++++++------ .../verification-token-form.component.ts | 20 ++++------- .../account/root/user-account-root.module.ts | 36 ++----------------- .../user/account/styles/_otp-login-form.scss | 2 +- 5 files changed, 23 insertions(+), 59 deletions(-) diff --git a/feature-libs/user/_index.scss b/feature-libs/user/_index.scss index fea387be8e1..586951f061c 100644 --- a/feature-libs/user/_index.scss +++ b/feature-libs/user/_index.scss @@ -9,7 +9,7 @@ $selectors: cx-address-book, cx-address-form, cx-suggested-addresses-dialog, cx-login, cx-login-form, cx-verification-token-form, cx-register, cx-reset-password, cx-close-account, cx-close-account-modal, cx-my-account-v2-profile, cx-my-account-v2-email, cx-my-account-v2-password, - cx-verification-token-form !default; + cx-otp-login-form !default; @each $selector in $selectors { #{$selector} { diff --git a/feature-libs/user/account/components/login-form/otp-login-form.component.ts b/feature-libs/user/account/components/login-form/otp-login-form.component.ts index 2d2ea34ab57..b07b1a426e2 100644 --- a/feature-libs/user/account/components/login-form/otp-login-form.component.ts +++ b/feature-libs/user/account/components/login-form/otp-login-form.component.ts @@ -15,7 +15,6 @@ import { GlobalMessageType, HttpErrorModel, RoutingService, - TranslationService, WindowRef, } from '@spartacus/core'; import { CustomFormValidators } from '@spartacus/storefront'; @@ -25,7 +24,7 @@ import { LoginForm, VerificationToken } from '../../root/model'; import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from './use-otp-login-form'; @Component({ - selector: 'cx-verification-token-form', + selector: 'cx-otp-login-form', templateUrl: './login-form.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -34,7 +33,6 @@ export class OneTimePasswordLoginFormComponent { protected routingService: RoutingService, protected verificationTokenFacade: VerificationTokenFacade, protected winRef: WindowRef, - protected translationService: TranslationService, protected globalMessage: GlobalMessageService ) {} @@ -98,14 +96,18 @@ export class OneTimePasswordLoginFormComponent { verificationToken: VerificationToken, loginForm: LoginForm ): void { - this.routingService.go({ - cxRoute: 'verifyToken', - params: { - loginId: loginForm.loginId, - password: loginForm.password, - tokenId: verificationToken.tokenId, + this.routingService.go( + { + cxRoute: 'verifyToken', }, - }); + { + state: { + loginId: loginForm.loginId, + password: loginForm.password, + tokenId: verificationToken.tokenId, + }, + } + ); } protected collectDataFromLoginForm(): LoginForm { diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index 19a231bb2be..98ccd4a8fe8 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -9,9 +9,9 @@ import { ChangeDetectorRef, Component, HostBinding, + OnInit, } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; @@ -20,10 +20,9 @@ import { VerificationTokenFormComponentService } from './verification-token-form templateUrl: './verification-token-form.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class VerificationTokenFormComponent { +export class VerificationTokenFormComponent implements OnInit { constructor( protected service: VerificationTokenFormComponentService, - private route: ActivatedRoute, private cdr: ChangeDetectorRef ) {} @@ -40,20 +39,15 @@ export class VerificationTokenFormComponent { password: string; - purpose: string; - waitTime: int = 60; isResendDisabled: boolean = true; ngOnInit() { - this.route.params.subscribe((params) => { - this.tokenId = params['tokenId']; - this.password = params['password']; - this.target = params['loginId']; - this.purpose = params['purpose']; - this.service.displayMessage(this.target); - }); + this.tokenId = history.state['tokenId']; + this.password = history.state['password']; + this.target = history.state['loginId']; + this.startWaitTimeInterval(); } @@ -65,7 +59,7 @@ export class VerificationTokenFormComponent { this.isResendDisabled = true; this.waitTime = 60; this.startWaitTimeInterval(); - this.service.sentOTP(this.target, this.password, this.purpose); + this.service.sentOTP(this.target, this.password, 'LOGIN'); this.service.displayMessage(this.target); } diff --git a/feature-libs/user/account/root/user-account-root.module.ts b/feature-libs/user/account/root/user-account-root.module.ts index ff53708d80d..ae5983064a5 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -33,39 +33,7 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { } @NgModule({ - imports: [ - UserAccountEventModule, - // RouterModule.forChild([ - // { - // // @ts-ignore - // path: null, - // canActivate: [AuthGuard, CmsPageGuard], - // component: PageLayoutComponent, - // data: { - // cxRoute: 'loginByVerifyToken', - // // cxContext: { - // // [ORDER_ENTRIES_CONTEXT]: SavedCartOrderEntriesContextToken, - // // }, - // }, - // }, - // ]), - ], - providers: [ - provideDefaultConfigFactory(defaultUserAccountComponentsConfig), - // provideDefaultConfig({ - // routing: { - // routes: { - // loginByVerifyToken: { - // paths: ['login/verify-token'], - // paramsMapping: { - // loginId: 'loginId', - // password: 'password', - // tokenId: 'tokenId', - // }, - // }, - // }, - // }, - // }), - ], + imports: [UserAccountEventModule], + providers: [provideDefaultConfigFactory(defaultUserAccountComponentsConfig)], }) export class UserAccountRootModule {} diff --git a/feature-libs/user/account/styles/_otp-login-form.scss b/feature-libs/user/account/styles/_otp-login-form.scss index c1950da6789..0ed3a2d68bd 100644 --- a/feature-libs/user/account/styles/_otp-login-form.scss +++ b/feature-libs/user/account/styles/_otp-login-form.scss @@ -1,4 +1,4 @@ -%cx-login-form { +%cx-otp-login-form { &.user-form { cx-spinner { display: none; From 5fc5dac203946d6d72cb3addc0ee6b6d4cf21a9c Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Wed, 17 Apr 2024 14:08:03 +0800 Subject: [PATCH 10/66] CXSPA-6689: add new method for otp login --- ...rification-token-form-component.service.ts | 2 +- .../src/auth/user-auth/facade/auth.service.ts | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts index c8d76d8cbfe..7dcb903df3c 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts @@ -50,7 +50,7 @@ export class VerificationTokenFormComponentService { this.busy$.next(true); from( - this.auth.loginWithCredentials( + this.auth.otpLoginWithCredentials( this.form.value.tokenId, this.form.value.tokenCode ) diff --git a/projects/core/src/auth/user-auth/facade/auth.service.ts b/projects/core/src/auth/user-auth/facade/auth.service.ts index 5045bb880e8..fec0089f076 100644 --- a/projects/core/src/auth/user-auth/facade/auth.service.ts +++ b/projects/core/src/auth/user-auth/facade/auth.service.ts @@ -31,7 +31,6 @@ export class AuthService { * Indicates whether the access token is being refreshed */ refreshInProgress$: Observable = new BehaviorSubject(false); - /** * Indicates whether the logout is being performed */ @@ -89,7 +88,6 @@ export class AuthService { */ async loginWithCredentials(userId: string, password: string): Promise { let uid = userId; - if (this.authMultisiteIsolationService) { uid = await lastValueFrom( this.authMultisiteIsolationService.decorateUserId(uid) @@ -111,6 +109,33 @@ export class AuthService { } catch {} } + /** + * Loads a new user token with otp tokenCode and otp tokenId. + * @param tokenId + * @param tokenCode + */ + async otpLoginWithCredentials( + tokenId: string, + tokenCode: string + ): Promise { + debugger; + let uid = tokenId; + + try { + await this.oAuthLibWrapperService.authorizeWithPasswordFlow( + uid, + tokenCode + ); + + // OCC specific user id handling. Customize when implementing different backend + this.userIdService.setUserId(OCC_USER_ID_CURRENT); + + this.store.dispatch(new AuthActions.Login()); + + this.authRedirectService.redirect(); + } catch {} + } + /** * Revokes tokens and clears state for logged user (tokens, userId). * To perform logout it is best to use `logout` method. Use this method with caution. From ab20750b5da976ebc9c844e7da03fa557062d084 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Wed, 17 Apr 2024 14:44:24 +0800 Subject: [PATCH 11/66] CXSPA-6689: bug fixed --- .../verification-token-form.component.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index 989a10ce7a9..cb0872ffed9 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -10,17 +10,11 @@ import { Component, ElementRef, HostBinding, - + OnInit, ViewChild, } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; -======= - OnInit, -} from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; ->>>>>>> feature/CXSPA-6672 import { Observable } from 'rxjs'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; @@ -33,7 +27,6 @@ export class VerificationTokenFormComponent implements OnInit { constructor( protected service: VerificationTokenFormComponentService, protected launchDialogService: LaunchDialogService, - private route: ActivatedRoute, private cdr: ChangeDetectorRef ) {} @@ -60,7 +53,6 @@ export class VerificationTokenFormComponent implements OnInit { this.tokenId = history.state['tokenId']; this.password = history.state['password']; this.target = history.state['loginId']; - this.startWaitTimeInterval(); } From 4e52f2240dd6b08fb31d96ce148b47f3b471d883 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Wed, 17 Apr 2024 17:27:14 +0800 Subject: [PATCH 12/66] add ut --- .../otp-login-form.component.spec.ts | 210 ++++++++++++++++++ .../connectors/user-account.connector.spec.ts | 22 ++ .../facade/verification-token.service.spec.ts | 62 ++++++ .../adapters/occ-user-account.adapter.spec.ts | 58 ++++- 4 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 feature-libs/user/account/components/login-form/otp-login-form.component.spec.ts create mode 100644 feature-libs/user/account/core/facade/verification-token.service.spec.ts diff --git a/feature-libs/user/account/components/login-form/otp-login-form.component.spec.ts b/feature-libs/user/account/components/login-form/otp-login-form.component.spec.ts new file mode 100644 index 00000000000..37026b51f35 --- /dev/null +++ b/feature-libs/user/account/components/login-form/otp-login-form.component.spec.ts @@ -0,0 +1,210 @@ +import { DebugElement, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { + GlobalMessageService, + I18nTestingModule, + WindowRef, +} from '@spartacus/core'; +import { FormErrorsModule, SpinnerModule } from '@spartacus/storefront'; +import { BehaviorSubject, of } from 'rxjs'; +import { VerificationTokenService } from '../../core/facade'; +import { VerificationTokenFacade } from '../../root/facade/verification-token.facade'; +import { LoginForm, VerificationToken } from '../../root/model'; +import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; +import createSpy = jasmine.createSpy; + +const isBusySubject = new BehaviorSubject(false); + +const formGroup: UntypedFormGroup = new UntypedFormGroup({ + userId: new UntypedFormControl(), + password: new UntypedFormControl(), +}); +const isUpdating$ = isBusySubject; + +const form: LoginForm = { + purpose: 'LOGIN', + loginId: 'mockEmail', + password: '1234', +}; + +const verificationToken: VerificationToken = { + expiresIn: '300', + tokenId: 'mockTokenId', +}; + +class MockWinRef { + get nativeWindow(): Window { + return {} as Window; + } +} + +class MockVerificationTokenService + implements Partial +{ + createVerificationToken = createSpy().and.callFake(() => + of(verificationToken) + ); +} + +class MockGlobalMessageService { + add = createSpy().and.stub(); + remove = createSpy().and.stub(); +} + +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} + +describe('OneTimePasswordLoginFormComponent', () => { + let component: OneTimePasswordLoginFormComponent; + let fixture: ComponentFixture; + let el: DebugElement; + let service: VerificationTokenFacade; + let winRef: WindowRef; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + FormErrorsModule, + SpinnerModule, + ], + declarations: [OneTimePasswordLoginFormComponent, MockUrlPipe], + providers: [ + { + provide: VerificationTokenFacade, + useClass: MockVerificationTokenService, + }, + { provide: WindowRef, useClass: MockWinRef }, + { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + winRef = TestBed.inject(WindowRef); + fixture = TestBed.createComponent(OneTimePasswordLoginFormComponent); + service = TestBed.inject(VerificationTokenService); + component = fixture.componentInstance; + el = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + describe('login', () => { + const userId = 'test@email.com'; + const password = 'secret'; + + it('should not patch user id', () => { + isUpdating$.subscribe().unsubscribe(); + expect(form.value.userId).toEqual(''); + }); + + it('should patch user id', () => { + spyOnProperty(winRef, 'nativeWindow', 'get').and.returnValue({ + history: { state: { newUid: 'test.user@shop.com' } }, + } as Window); + isUpdating$.subscribe().unsubscribe(); + expect(formGroup.value.userId).toEqual('test.user@shop.com'); + }); + + describe('success', () => { + it('should request email', () => { + component.onSubmit(); + expect(service.createVerificationToken).toHaveBeenCalledWith( + userId, + password + ); + }); + + it('should reset the form', () => { + spyOn(formGroup, 'reset').and.stub(); + service.createVerificationToken(form); + expect(formGroup.reset).toHaveBeenCalled(); + }); + }); + + describe('error', () => { + beforeEach(() => { + formGroup.setValue({ + userId: 'invalid', + password: '123', + }); + }); + + it('should not login', () => { + component.onSubmit(); + expect(service.createVerificationToken).not.toHaveBeenCalled(); + }); + + it('should not reset the form', () => { + spyOn(formGroup, 'reset').and.stub(); + component.onSubmit(); + expect(formGroup.reset).not.toHaveBeenCalled(); + }); + }); + }); + + describe('busy', () => { + it('should disable the submit button when form is disabled', () => { + component.form.disable(); + fixture.detectChanges(); + const submitBtn: HTMLButtonElement = el.query( + By.css('button') + ).nativeElement; + expect(submitBtn.disabled).toBeTruthy(); + }); + + it('should show the spinner', () => { + isBusySubject.next(true); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + }); + + describe('idle', () => { + it('should enable the submit button', () => { + component.form.enable(); + fixture.detectChanges(); + const submitBtn = el.query(By.css('button')); + expect(submitBtn.nativeElement.disabled).toBeFalsy(); + }); + + it('should not show the spinner', () => { + isBusySubject.next(false); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeNull(); + }); + }); + + describe('Form Interactions', () => { + it('should call onSubmit() method on submit', () => { + const request = spyOn(component, 'onSubmit'); + const form = el.query(By.css('form')); + form.triggerEventHandler('submit', null); + expect(request).toHaveBeenCalled(); + }); + + it('should call the service method on submit', () => { + component.onSubmit(); + expect(service.createVerificationToken).toHaveBeenCalled(); + }); + }); +}); diff --git a/feature-libs/user/account/core/connectors/user-account.connector.spec.ts b/feature-libs/user/account/core/connectors/user-account.connector.spec.ts index 43cfb56ab4d..bcfaa0e2e6a 100644 --- a/feature-libs/user/account/core/connectors/user-account.connector.spec.ts +++ b/feature-libs/user/account/core/connectors/user-account.connector.spec.ts @@ -1,10 +1,25 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; +import { LoginForm, VerificationToken } from '../../root/model'; import { UserAccountAdapter } from './user-account.adapter'; import { UserAccountConnector } from './user-account.connector'; import createSpy = jasmine.createSpy; +const form: LoginForm = { + purpose: 'LOGIN', + loginId: 'mockEmail', + password: '1234', +}; + +const verificationToken: VerificationToken = { + expiresIn: '300', + tokenId: 'mockTokenId', +}; + class MockUserAdapter implements UserAccountAdapter { + createVerificationToken = createSpy('createVerificationToken').and.callFake( + () => of(verificationToken) + ); load = createSpy('load').and.callFake((userId) => of(`load-${userId}`)); } @@ -34,4 +49,11 @@ describe('UserConnector', () => { expect(result).toEqual('load-user-id'); expect(adapter.load).toHaveBeenCalledWith('user-id'); }); + + it('should create a new customer', () => { + let result; + service.createVerificationToken(form).subscribe((res) => (result = res)); + expect(result).toEqual(verificationToken); + expect(adapter.createVerificationToken).toHaveBeenCalledWith(form); + }); }); diff --git a/feature-libs/user/account/core/facade/verification-token.service.spec.ts b/feature-libs/user/account/core/facade/verification-token.service.spec.ts new file mode 100644 index 00000000000..d1b114b57f2 --- /dev/null +++ b/feature-libs/user/account/core/facade/verification-token.service.spec.ts @@ -0,0 +1,62 @@ +import { TestBed } from '@angular/core/testing'; +import { CommandService } from '@spartacus/core'; +import { + UserAccountConnector, + VerificationTokenService, +} from '@spartacus/user/account/core'; +import { LoginForm, VerificationToken } from '@spartacus/user/account/root'; +import { of } from 'rxjs'; +import createSpy = jasmine.createSpy; + +const form: LoginForm = { + purpose: 'LOGIN', + loginId: 'mockEmail', + password: '1234', +}; + +const verificationToken: VerificationToken = { + expiresIn: '300', + tokenId: 'mockTokenId', +}; + +class MockUserAccountConnector implements Partial { + createVerificationToken = createSpy().and.callFake(() => + of(verificationToken) + ); +} + +describe('VerificationTokenService', () => { + let service: VerificationTokenService; + let connector: UserAccountConnector; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: UserAccountConnector, useClass: MockUserAccountConnector }, + CommandService, + VerificationTokenService, + ], + }); + + service = TestBed.inject(VerificationTokenService); + connector = TestBed.inject(UserAccountConnector); + }); + + it('should inject VerificationTokenService', () => { + expect(service).toBeTruthy(); + }); + + describe('create verification token', () => { + it('should create verification token for given email and password', () => { + let result: VerificationToken | undefined; + service + .createVerificationToken(form) + .subscribe((data) => { + result = data; + }) + .unsubscribe(); + expect(result).toEqual(verificationToken); + expect(connector.createVerificationToken).toHaveBeenCalledWith(form); + }); + }); +}); diff --git a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts index 180ed4dd8d8..2cac5c49d93 100644 --- a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts +++ b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts @@ -10,8 +10,15 @@ import { OccConfig, OccEndpointsService, } from '@spartacus/core'; -import { User } from '@spartacus/user/account/root'; -import { USER_ACCOUNT_NORMALIZER } from '@spartacus/user/account/core'; +import { + USER_ACCOUNT_NORMALIZER, + VERIFICATION_TOKEN_NORMALIZER, +} from '@spartacus/user/account/core'; +import { + LoginForm, + User, + VerificationToken, +} from '@spartacus/user/account/root'; import { OccUserAccountAdapter } from './occ-user-account.adapter'; export const mockOccModuleConfig: OccConfig = { @@ -54,6 +61,17 @@ const user: User = { displayUid: password, }; +const form: LoginForm = { + purpose: 'LOGIN', + loginId: 'mockEmail', + password: password, +}; + +const verificationToken: VerificationToken = { + expiresIn: '300', + tokenId: 'mockTokenId', +}; + describe('OccUserAccountAdapter', () => { let occUserAccountAdapter: OccUserAccountAdapter; let httpMock: HttpTestingController; @@ -115,4 +133,40 @@ describe('OccUserAccountAdapter', () => { expect(converter.pipeable).toHaveBeenCalledWith(USER_ACCOUNT_NORMALIZER); }); }); + + describe('create verification token', () => { + it('should create verification token for given email and password', () => { + occUserAccountAdapter + .createVerificationToken(form) + .subscribe((result) => { + expect(result).toEqual(verificationToken); + }); + + const mockReq = httpMock.expectOne((req) => { + return req.method === 'POST'; + }); + + expect(occEndpointsService.buildUrl).toHaveBeenCalledWith( + 'createVerificationToken', + { + state: { form }, + } + ); + expect(mockReq.cancelled).toBeFalsy(); + expect(mockReq.request.responseType).toEqual('json'); + mockReq.flush(verificationToken); + }); + + it('should use converter', () => { + occUserAccountAdapter.createVerificationToken(form).subscribe(); + httpMock + .expectOne((req) => { + return req.method === 'POST'; + }) + .flush(verificationToken); + expect(converter.pipeable).toHaveBeenCalledWith( + VERIFICATION_TOKEN_NORMALIZER + ); + }); + }); }); From cdb17c54244b71b7eff4b61eeb16a579b8feebd1 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Thu, 18 Apr 2024 11:19:41 +0800 Subject: [PATCH 13/66] add otp cms component --- .../account/components/login-form/index.ts | 2 - .../login-form/login-form.module.ts | 21 +------ .../login-form/use-otp-login-form.ts | 14 ----- .../components/otp-login-form/index.ts | 8 +++ .../otp-login-form.component.html | 60 +++++++++++++++++++ .../otp-login-form.component.spec.ts | 0 .../otp-login-form.component.ts | 6 +- .../otp-login-form/otp-login-form.module.ts | 52 ++++++++++++++++ .../user/account/components/public_api.ts | 4 +- .../user-account-component.module.ts | 3 +- .../components/user-account-constants.ts | 7 +++ .../verification-token-form.component.ts | 7 ++- .../verification-token-form.module.ts | 3 +- .../account/root/user-account-root.module.ts | 1 + .../features/user/user-feature.module.ts | 8 +-- 15 files changed, 150 insertions(+), 46 deletions(-) delete mode 100644 feature-libs/user/account/components/login-form/use-otp-login-form.ts create mode 100644 feature-libs/user/account/components/otp-login-form/index.ts create mode 100644 feature-libs/user/account/components/otp-login-form/otp-login-form.component.html rename feature-libs/user/account/components/{login-form => otp-login-form}/otp-login-form.component.spec.ts (100%) rename feature-libs/user/account/components/{login-form => otp-login-form}/otp-login-form.component.ts (96%) create mode 100644 feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts create mode 100644 feature-libs/user/account/components/user-account-constants.ts diff --git a/feature-libs/user/account/components/login-form/index.ts b/feature-libs/user/account/components/login-form/index.ts index 73b08aa59d2..81181c5629d 100644 --- a/feature-libs/user/account/components/login-form/index.ts +++ b/feature-libs/user/account/components/login-form/index.ts @@ -7,5 +7,3 @@ export * from './login-form-component.service'; export * from './login-form.component'; export * from './login-form.module'; -export * from './otp-login-form.component'; -export * from './use-otp-login-form'; diff --git a/feature-libs/user/account/components/login-form/login-form.module.ts b/feature-libs/user/account/components/login-form/login-form.module.ts index eff4bbd6892..cc6a01fd67f 100644 --- a/feature-libs/user/account/components/login-form/login-form.module.ts +++ b/feature-libs/user/account/components/login-form/login-form.module.ts @@ -5,7 +5,7 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule, inject } from '@angular/core'; +import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { @@ -18,7 +18,6 @@ import { UrlModule, WindowRef, provideDefaultConfig, - provideDefaultConfigFactory, } from '@spartacus/core'; import { FormErrorsModule, @@ -27,17 +26,6 @@ import { } from '@spartacus/storefront'; import { LoginFormComponentService } from './login-form-component.service'; import { LoginFormComponent } from './login-form.component'; -import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; - -import { USE_ONE_TIME_PASSWORD_LOGIN } from './use-otp-login-form'; - -const oneTimePasswordLoginFormMapping: CmsConfig = { - cmsComponents: { - ReturningCustomerLoginComponent: { - component: OneTimePasswordLoginFormComponent, - }, - }, -}; @NgModule({ imports: [ @@ -68,11 +56,8 @@ const oneTimePasswordLoginFormMapping: CmsConfig = { }, }, }), - provideDefaultConfigFactory(() => - inject(USE_ONE_TIME_PASSWORD_LOGIN) ? oneTimePasswordLoginFormMapping : {} - ), ], - declarations: [LoginFormComponent, OneTimePasswordLoginFormComponent], - exports: [LoginFormComponent, OneTimePasswordLoginFormComponent], + declarations: [LoginFormComponent], + exports: [LoginFormComponent], }) export class LoginFormModule {} diff --git a/feature-libs/user/account/components/login-form/use-otp-login-form.ts b/feature-libs/user/account/components/login-form/use-otp-login-form.ts deleted file mode 100644 index 3f9e29cf811..00000000000 --- a/feature-libs/user/account/components/login-form/use-otp-login-form.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -import { InjectionToken } from '@angular/core'; - -export const USE_ONE_TIME_PASSWORD_LOGIN = new InjectionToken( - 'feature flag to enable enhanced UI related pages login', - { providedIn: 'root', factory: () => false } -); - -export const ONE_TIME_PASSWORD_LOGIN_PURPOSE = 'LOGIN'; diff --git a/feature-libs/user/account/components/otp-login-form/index.ts b/feature-libs/user/account/components/otp-login-form/index.ts new file mode 100644 index 00000000000..f34c35cd3f4 --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './otp-login-form.component'; +export * from './otp-login-form.module'; diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.html b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.html new file mode 100644 index 00000000000..be40dacc4ba --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.html @@ -0,0 +1,60 @@ + + +
+ +

+ {{ 'formLegend.required' | cxTranslate }} +

+ + + + + + {{ 'loginForm.forgotPassword' | cxTranslate }} + + + +
+ + + * + diff --git a/feature-libs/user/account/components/login-form/otp-login-form.component.spec.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts similarity index 100% rename from feature-libs/user/account/components/login-form/otp-login-form.component.spec.ts rename to feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts diff --git a/feature-libs/user/account/components/login-form/otp-login-form.component.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts similarity index 96% rename from feature-libs/user/account/components/login-form/otp-login-form.component.ts rename to feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts index b07b1a426e2..310cfc4fdf6 100644 --- a/feature-libs/user/account/components/login-form/otp-login-form.component.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts @@ -21,11 +21,13 @@ import { CustomFormValidators } from '@spartacus/storefront'; import { BehaviorSubject, Observable, tap } from 'rxjs'; import { VerificationTokenFacade } from '../../root/facade/verification-token.facade'; import { LoginForm, VerificationToken } from '../../root/model'; -import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from './use-otp-login-form'; +import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from '../user-account-constants'; + + @Component({ selector: 'cx-otp-login-form', - templateUrl: './login-form.component.html', + templateUrl: './otp-login-form.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class OneTimePasswordLoginFormComponent { diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts new file mode 100644 index 00000000000..41bda3c119d --- /dev/null +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { + CmsConfig, + FeaturesConfigModule, + I18nModule, + NotAuthGuard, + UrlModule, + provideDefaultConfig, +} from '@spartacus/core'; +import { + FormErrorsModule, + PasswordVisibilityToggleModule, + SpinnerModule, +} from '@spartacus/storefront'; +import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule, + UrlModule, + I18nModule, + FormErrorsModule, + SpinnerModule, + PasswordVisibilityToggleModule, + FeaturesConfigModule, + ], + providers: [ + provideDefaultConfig({ + cmsComponents: { + ReturningCustomerOTPLoginComponent: { + component: OneTimePasswordLoginFormComponent, + guards: [NotAuthGuard], + }, + }, + }), + ], + declarations: [OneTimePasswordLoginFormComponent], + exports: [OneTimePasswordLoginFormComponent], +}) +export class OneTimePasswordLoginFormModeule {} diff --git a/feature-libs/user/account/components/public_api.ts b/feature-libs/user/account/components/public_api.ts index 6d7429113b3..a2014b0e5d1 100644 --- a/feature-libs/user/account/components/public_api.ts +++ b/feature-libs/user/account/components/public_api.ts @@ -7,5 +7,7 @@ export * from './login-form/index'; export * from './login-register/index'; export * from './login/index'; -export * from './user-account-component.module'; export * from './my-account-v2-user/index'; +export * from './otp-login-form/index'; +export * from './user-account-component.module'; +export * from './verification-token-form/index'; diff --git a/feature-libs/user/account/components/user-account-component.module.ts b/feature-libs/user/account/components/user-account-component.module.ts index 16477be59d3..406046ad9af 100644 --- a/feature-libs/user/account/components/user-account-component.module.ts +++ b/feature-libs/user/account/components/user-account-component.module.ts @@ -11,15 +11,16 @@ import { VerificationTokenFormModule } from './verification-token-form/verificat import { LoginRegisterModule } from './login-register/login-register.module'; import { LoginModule } from './login/login.module'; import { MyAccountV2UserModule } from './my-account-v2-user'; +import { OneTimePasswordLoginFormModeule } from './otp-login-form'; @NgModule({ imports: [ LoginModule, LoginFormModule, VerificationTokenFormModule, - LoginRegisterModule, MyAccountV2UserModule, + OneTimePasswordLoginFormModeule, ], }) export class UserAccountComponentsModule {} diff --git a/feature-libs/user/account/components/user-account-constants.ts b/feature-libs/user/account/components/user-account-constants.ts new file mode 100644 index 00000000000..7dfa2a0cdd7 --- /dev/null +++ b/feature-libs/user/account/components/user-account-constants.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ONE_TIME_PASSWORD_LOGIN_PURPOSE = 'LOGIN'; diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index cb0872ffed9..f6b9a1e4ac8 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -16,6 +16,7 @@ import { import { UntypedFormGroup } from '@angular/forms'; import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; import { Observable } from 'rxjs'; +import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from '../user-account-constants'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; @Component({ @@ -64,7 +65,11 @@ export class VerificationTokenFormComponent implements OnInit { this.isResendDisabled = true; this.waitTime = 60; this.startWaitTimeInterval(); - this.service.sentOTP(this.target, this.password, 'LOGIN'); + this.service.sentOTP( + this.target, + this.password, + ONE_TIME_PASSWORD_LOGIN_PURPOSE + ); this.service.displayMessage(this.target); } diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts index f97053944a5..1fc2f50d6ad 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts @@ -29,6 +29,7 @@ import { defaultVerificationTokenLayoutConfig } from './default-verification-tok import { VerificationTokenDialogComponent } from './verification-token-dialog.component'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; import { VerificationTokenFormComponent } from './verification-token-form.component'; +import { VerificationTokenFacade } from '../../root/facade'; @NgModule({ imports: [ @@ -54,7 +55,7 @@ import { VerificationTokenFormComponent } from './verification-token-form.compon { provide: VerificationTokenFormComponentService, useClass: VerificationTokenFormComponentService, - deps: [AuthService, GlobalMessageService, WindowRef], + deps: [AuthService, GlobalMessageService, VerificationTokenFacade, WindowRef], }, ], }, diff --git a/feature-libs/user/account/root/user-account-root.module.ts b/feature-libs/user/account/root/user-account-root.module.ts index ae5983064a5..398847c60d0 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -23,6 +23,7 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { 'VerifyOTPTokenComponent', 'ReturningCustomerRegisterComponent', 'MyAccountViewUserComponent', + 'ReturningCustomerOTPLoginComponent', ], }, // by default core is bundled together with components diff --git a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts index 57d38ef58a9..e0ad639bd1b 100644 --- a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts +++ b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts @@ -11,8 +11,8 @@ import { userAccountTranslations, } from '@spartacus/user/account/assets'; import { - UserAccountRootModule, USER_ACCOUNT_FEATURE, + UserAccountRootModule, } from '@spartacus/user/account/root'; import { userProfileTranslationChunksConfig, @@ -28,7 +28,7 @@ import { USER_PROFILE_FEATURE, } from '@spartacus/user/profile/root'; import { environment } from '../../../../environments/environment'; -import { USE_ONE_TIME_PASSWORD_LOGIN } from '@spartacus/user/account/components'; + @NgModule({ @@ -73,10 +73,6 @@ import { USE_ONE_TIME_PASSWORD_LOGIN } from '@spartacus/user/account/components' provide: USE_MY_ACCOUNT_V2_PASSWORD, useValue: environment.myAccountV2, }, - { - provide: USE_ONE_TIME_PASSWORD_LOGIN, - useValue: environment.otpUserLogin, - }, provideConfig({ i18n: { resources: userProfileTranslations, From b30b17cc48bf4624563ec7da0ab8eb3b1955268c Mon Sep 17 00:00:00 2001 From: niehuayang Date: Thu, 18 Apr 2024 11:32:18 +0800 Subject: [PATCH 14/66] remove env var --- .../src/app/spartacus/features/user/user-feature.module.ts | 4 +--- projects/storefrontapp/src/environments/environment.prod.ts | 1 - projects/storefrontapp/src/environments/environment.ts | 1 - .../src/environments/models/build.process.env.d.ts | 1 - .../src/environments/models/environment.model.ts | 1 - 5 files changed, 1 insertion(+), 7 deletions(-) diff --git a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts index e0ad639bd1b..b948e9a0674 100644 --- a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts +++ b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts @@ -11,8 +11,8 @@ import { userAccountTranslations, } from '@spartacus/user/account/assets'; import { - USER_ACCOUNT_FEATURE, UserAccountRootModule, + USER_ACCOUNT_FEATURE, } from '@spartacus/user/account/root'; import { userProfileTranslationChunksConfig, @@ -29,8 +29,6 @@ import { } from '@spartacus/user/profile/root'; import { environment } from '../../../../environments/environment'; - - @NgModule({ imports: [UserAccountRootModule, UserProfileRootModule], providers: [ diff --git a/projects/storefrontapp/src/environments/environment.prod.ts b/projects/storefrontapp/src/environments/environment.prod.ts index 7e6e81a28c2..73df861f676 100644 --- a/projects/storefrontapp/src/environments/environment.prod.ts +++ b/projects/storefrontapp/src/environments/environment.prod.ts @@ -22,5 +22,4 @@ export const environment: Environment = { requestedDeliveryDate: buildProcess.env.CX_REQUESTED_DELIVERY_DATE, pdfInvoices: buildProcess.env.CX_PDF_INVOICES, myAccountV2: buildProcess.env.CX_MY_ACCOUNT_V2 ?? false, - otpUserLogin: buildProcess.env.CX_OTP_USER_LOGIN ?? false, }; diff --git a/projects/storefrontapp/src/environments/environment.ts b/projects/storefrontapp/src/environments/environment.ts index 182485edb5f..5ac06fae96e 100644 --- a/projects/storefrontapp/src/environments/environment.ts +++ b/projects/storefrontapp/src/environments/environment.ts @@ -35,5 +35,4 @@ export const environment: Environment = { requestedDeliveryDate: buildProcess.env.CX_REQUESTED_DELIVERY_DATE ?? false, pdfInvoices: buildProcess.env.CX_PDF_INVOICES ?? false, myAccountV2: buildProcess.env.CX_MY_ACCOUNT_V2 ?? false, - otpUserLogin: buildProcess.env.CX_OTP_USER_LOGIN ?? false, }; diff --git a/projects/storefrontapp/src/environments/models/build.process.env.d.ts b/projects/storefrontapp/src/environments/models/build.process.env.d.ts index 6b4cf23c8f6..3e52ae56530 100644 --- a/projects/storefrontapp/src/environments/models/build.process.env.d.ts +++ b/projects/storefrontapp/src/environments/models/build.process.env.d.ts @@ -24,5 +24,4 @@ interface Env { CX_REQUESTED_DELIVERY_DATE: boolean; CX_PDF_INVOICES: boolean; CX_MY_ACCOUNT_V2: boolean; - CX_OTP_USER_LOGIN: boolean; } diff --git a/projects/storefrontapp/src/environments/models/environment.model.ts b/projects/storefrontapp/src/environments/models/environment.model.ts index a84a39ed7c7..754f3f7d5c2 100644 --- a/projects/storefrontapp/src/environments/models/environment.model.ts +++ b/projects/storefrontapp/src/environments/models/environment.model.ts @@ -20,5 +20,4 @@ export interface Environment { requestedDeliveryDate: boolean; pdfInvoices: boolean; myAccountV2: boolean; - otpUserLogin: boolean; } From 6ba6f02fab598b023d3a48da1226673e066304d0 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Thu, 18 Apr 2024 16:02:15 +0800 Subject: [PATCH 15/66] refine code --- .../components/login-form/login-form.module.ts | 1 - .../otp-login-form.component.spec.ts | 9 ++++++--- .../otp-login-form/otp-login-form.component.ts | 10 ++++++---- .../otp-login-form/otp-login-form.module.ts | 1 - .../verification-token-form-component.service.ts | 2 +- feature-libs/user/account/root/model/index.ts | 1 + .../user/account/root/model/otp-login.model.ts | 16 ++++++++++++++++ .../user/account/root/model/user.model.ts | 12 ------------ 8 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 feature-libs/user/account/root/model/otp-login.model.ts diff --git a/feature-libs/user/account/components/login-form/login-form.module.ts b/feature-libs/user/account/components/login-form/login-form.module.ts index cc6a01fd67f..f89dd4404c1 100644 --- a/feature-libs/user/account/components/login-form/login-form.module.ts +++ b/feature-libs/user/account/components/login-form/login-form.module.ts @@ -58,6 +58,5 @@ import { LoginFormComponent } from './login-form.component'; }), ], declarations: [LoginFormComponent], - exports: [LoginFormComponent], }) export class LoginFormModule {} diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts index 37026b51f35..18ab74fc904 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts @@ -13,10 +13,13 @@ import { WindowRef, } from '@spartacus/core'; import { FormErrorsModule, SpinnerModule } from '@spartacus/storefront'; +import { VerificationTokenService } from '@spartacus/user/account/core'; +import { + LoginForm, + VerificationToken, + VerificationTokenFacade, +} from '@spartacus/user/account/root'; import { BehaviorSubject, of } from 'rxjs'; -import { VerificationTokenService } from '../../core/facade'; -import { VerificationTokenFacade } from '../../root/facade/verification-token.facade'; -import { LoginForm, VerificationToken } from '../../root/model'; import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; import createSpy = jasmine.createSpy; diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts index 310cfc4fdf6..8efd2cff970 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts @@ -19,11 +19,13 @@ import { } from '@spartacus/core'; import { CustomFormValidators } from '@spartacus/storefront'; import { BehaviorSubject, Observable, tap } from 'rxjs'; -import { VerificationTokenFacade } from '../../root/facade/verification-token.facade'; -import { LoginForm, VerificationToken } from '../../root/model'; -import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from '../user-account-constants'; - +import { + LoginForm, + VerificationToken, + VerificationTokenFacade, +} from '@spartacus/user/account/root'; +import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from '../user-account-constants'; @Component({ selector: 'cx-otp-login-form', diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts index 41bda3c119d..316534fc7d0 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.module.ts @@ -47,6 +47,5 @@ import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; }), ], declarations: [OneTimePasswordLoginFormComponent], - exports: [OneTimePasswordLoginFormComponent], }) export class OneTimePasswordLoginFormModeule {} diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts index 7dcb903df3c..b190740e6f9 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts @@ -16,9 +16,9 @@ import { GlobalMessageType, WindowRef, } from '@spartacus/core'; +import { VerificationTokenFacade } from '@spartacus/user/account/root'; import { BehaviorSubject, from } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; -import { VerificationTokenFacade } from '../../root/facade'; @Injectable() export class VerificationTokenFormComponentService { diff --git a/feature-libs/user/account/root/model/index.ts b/feature-libs/user/account/root/model/index.ts index 5b89674523c..beafee30f70 100644 --- a/feature-libs/user/account/root/model/index.ts +++ b/feature-libs/user/account/root/model/index.ts @@ -5,4 +5,5 @@ */ export * from './augmented-core.model'; +export * from './otp-login.model'; export * from './user.model'; diff --git a/feature-libs/user/account/root/model/otp-login.model.ts b/feature-libs/user/account/root/model/otp-login.model.ts new file mode 100644 index 00000000000..7591837865a --- /dev/null +++ b/feature-libs/user/account/root/model/otp-login.model.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface LoginForm { + purpose: string; + loginId: string; + password: string; +} + +export interface VerificationToken { + expiresIn: string; + tokenId: string; +} diff --git a/feature-libs/user/account/root/model/user.model.ts b/feature-libs/user/account/root/model/user.model.ts index d89ba2eeb9a..976fa7fe4f1 100644 --- a/feature-libs/user/account/root/model/user.model.ts +++ b/feature-libs/user/account/root/model/user.model.ts @@ -13,15 +13,3 @@ export interface User { customerId?: string; title?: string; } - -export interface LoginForm { - purpose: string; - loginId: string; - password: string; -} - -export interface VerificationToken { - expiresIn: string; - tokenId: string; -} - From b3ccbfcee8966f13f32e6d2b9636e535b129d29d Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Thu, 18 Apr 2024 16:07:57 +0800 Subject: [PATCH 16/66] CXSPA-6689: code refined --- projects/core/src/auth/user-auth/facade/auth.service.ts | 6 ++---- .../src/auth/user-auth/services/auth-redirect.service.ts | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/core/src/auth/user-auth/facade/auth.service.ts b/projects/core/src/auth/user-auth/facade/auth.service.ts index fec0089f076..eb4a3e56baa 100644 --- a/projects/core/src/auth/user-auth/facade/auth.service.ts +++ b/projects/core/src/auth/user-auth/facade/auth.service.ts @@ -118,12 +118,9 @@ export class AuthService { tokenId: string, tokenCode: string ): Promise { - debugger; - let uid = tokenId; - try { await this.oAuthLibWrapperService.authorizeWithPasswordFlow( - uid, + tokenId, tokenCode ); @@ -132,6 +129,7 @@ export class AuthService { this.store.dispatch(new AuthActions.Login()); + debugger; this.authRedirectService.redirect(); } catch {} } diff --git a/projects/core/src/auth/user-auth/services/auth-redirect.service.ts b/projects/core/src/auth/user-auth/services/auth-redirect.service.ts index 09829e8173b..587f0342ac3 100644 --- a/projects/core/src/auth/user-auth/services/auth-redirect.service.ts +++ b/projects/core/src/auth/user-auth/services/auth-redirect.service.ts @@ -65,8 +65,10 @@ export class AuthRedirectService implements OnDestroy { .pipe(take(1)) .subscribe((redirectUrl) => { if (redirectUrl === undefined) { + debugger; this.routing.go('/'); } else { + debugger; this.routing.goByUrl(redirectUrl); } this.clearRedirectUrl(); From 131b5d65d2f9b6cdc1481a9301566fb65bb45820 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Fri, 19 Apr 2024 14:46:25 +0800 Subject: [PATCH 17/66] add ut --- .../otp-login-form.component.spec.ts | 75 ++++++++++--------- .../otp-login-form.component.ts | 39 +++++----- .../user-account-component.module.ts | 5 +- .../connectors/user-account.connector.spec.ts | 2 +- .../facade/verification-token.service.spec.ts | 2 +- .../adapters/occ-user-account.adapter.spec.ts | 7 +- 6 files changed, 66 insertions(+), 64 deletions(-) diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts index 18ab74fc904..18a398e0b3b 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts @@ -1,15 +1,12 @@ import { DebugElement, Pipe, PipeTransform } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { - ReactiveFormsModule, - UntypedFormControl, - UntypedFormGroup, -} from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { GlobalMessageService, I18nTestingModule, + RoutingService, WindowRef, } from '@spartacus/core'; import { FormErrorsModule, SpinnerModule } from '@spartacus/storefront'; @@ -19,21 +16,13 @@ import { VerificationToken, VerificationTokenFacade, } from '@spartacus/user/account/root'; -import { BehaviorSubject, of } from 'rxjs'; +import { of } from 'rxjs'; import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; import createSpy = jasmine.createSpy; -const isBusySubject = new BehaviorSubject(false); - -const formGroup: UntypedFormGroup = new UntypedFormGroup({ - userId: new UntypedFormControl(), - password: new UntypedFormControl(), -}); -const isUpdating$ = isBusySubject; - -const form: LoginForm = { +const loginForm: LoginForm = { purpose: 'LOGIN', - loginId: 'mockEmail', + loginId: 'test@email.com', password: '1234', }; @@ -61,6 +50,10 @@ class MockGlobalMessageService { remove = createSpy().and.stub(); } +class MockRoutingService implements Partial { + go = () => Promise.resolve(true); +} + @Pipe({ name: 'cxUrl', }) @@ -93,6 +86,7 @@ describe('OneTimePasswordLoginFormComponent', () => { }, { provide: WindowRef, useClass: MockWinRef }, { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + { provide: RoutingService, useClass: MockRoutingService }, ], }).compileComponents(); }) @@ -101,7 +95,7 @@ describe('OneTimePasswordLoginFormComponent', () => { beforeEach(() => { winRef = TestBed.inject(WindowRef); fixture = TestBed.createComponent(OneTimePasswordLoginFormComponent); - service = TestBed.inject(VerificationTokenService); + service = TestBed.inject(VerificationTokenFacade); component = fixture.componentInstance; el = fixture.debugElement; fixture.detectChanges(); @@ -112,41 +106,42 @@ describe('OneTimePasswordLoginFormComponent', () => { }); describe('login', () => { - const userId = 'test@email.com'; - const password = 'secret'; - it('should not patch user id', () => { - isUpdating$.subscribe().unsubscribe(); - expect(form.value.userId).toEqual(''); + component.isUpdating$.subscribe().unsubscribe(); + expect(component.form.value.userId).toEqual(''); }); it('should patch user id', () => { spyOnProperty(winRef, 'nativeWindow', 'get').and.returnValue({ - history: { state: { newUid: 'test.user@shop.com' } }, + history: { state: { newUid: loginForm.loginId } }, } as Window); - isUpdating$.subscribe().unsubscribe(); - expect(formGroup.value.userId).toEqual('test.user@shop.com'); + component.isUpdating$.subscribe().unsubscribe(); + expect(component.form.value.userId).toEqual(loginForm.loginId); }); describe('success', () => { + beforeEach(() => { + component.form.setValue({ + userId: loginForm.loginId, + password: loginForm.password, + }); + }); + it('should request email', () => { component.onSubmit(); - expect(service.createVerificationToken).toHaveBeenCalledWith( - userId, - password - ); + expect(service.createVerificationToken).toHaveBeenCalledWith(loginForm); }); it('should reset the form', () => { - spyOn(formGroup, 'reset').and.stub(); - service.createVerificationToken(form); - expect(formGroup.reset).toHaveBeenCalled(); + spyOn(component.form, 'reset').and.stub(); + component.onSubmit(); + expect(component.form.reset).toHaveBeenCalled(); }); }); describe('error', () => { beforeEach(() => { - formGroup.setValue({ + component.form.setValue({ userId: 'invalid', password: '123', }); @@ -158,9 +153,9 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should not reset the form', () => { - spyOn(formGroup, 'reset').and.stub(); + spyOn(component.form, 'reset').and.stub(); component.onSubmit(); - expect(formGroup.reset).not.toHaveBeenCalled(); + expect(component.form.reset).not.toHaveBeenCalled(); }); }); }); @@ -176,7 +171,8 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should show the spinner', () => { - isBusySubject.next(true); + // @ts-ignore + component.busy$.next(true); fixture.detectChanges(); expect(el.query(By.css('cx-spinner'))).toBeTruthy(); }); @@ -191,7 +187,8 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should not show the spinner', () => { - isBusySubject.next(false); + // @ts-ignore + component.isUpdating$.next(false); fixture.detectChanges(); expect(el.query(By.css('cx-spinner'))).toBeNull(); }); @@ -206,6 +203,10 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should call the service method on submit', () => { + component.form.setValue({ + userId: loginForm.loginId, + password: loginForm.password, + }); component.onSubmit(); expect(service.createVerificationToken).toHaveBeenCalled(); }); diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts index 8efd2cff970..b82f7084b72 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts @@ -75,10 +75,28 @@ export class OneTimePasswordLoginFormComponent { this.goToVerificationTokenForm(result, loginForm), error: (error: HttpErrorModel) => this.onCreateVerificationTokenFail(error), - complete: () => this.busy$.next(false), + complete: () => this.onCreateVerificationTokenComplete(), }); } + protected goToVerificationTokenForm( + verificationToken: VerificationToken, + loginForm: LoginForm + ): void { + this.routingService.go( + { + cxRoute: 'verifyToken', + }, + { + state: { + loginId: loginForm.loginId, + password: loginForm.password, + tokenId: verificationToken.tokenId, + }, + } + ); + } + protected onCreateVerificationTokenFail(error: HttpErrorModel): void { this.busy$.next(false); const errorDetails = error.details ?? []; @@ -96,22 +114,9 @@ export class OneTimePasswordLoginFormComponent { }); } - protected goToVerificationTokenForm( - verificationToken: VerificationToken, - loginForm: LoginForm - ): void { - this.routingService.go( - { - cxRoute: 'verifyToken', - }, - { - state: { - loginId: loginForm.loginId, - password: loginForm.password, - tokenId: verificationToken.tokenId, - }, - } - ); + protected onCreateVerificationTokenComplete(): void { + this.form.reset(); + this.busy$.next(false); } protected collectDataFromLoginForm(): LoginForm { diff --git a/feature-libs/user/account/components/user-account-component.module.ts b/feature-libs/user/account/components/user-account-component.module.ts index 406046ad9af..d0cecd90fef 100644 --- a/feature-libs/user/account/components/user-account-component.module.ts +++ b/feature-libs/user/account/components/user-account-component.module.ts @@ -6,12 +6,11 @@ import { NgModule } from '@angular/core'; import { LoginFormModule } from './login-form/login-form.module'; -import { VerificationTokenFormModule } from './verification-token-form/verification-token-form.module'; - import { LoginRegisterModule } from './login-register/login-register.module'; import { LoginModule } from './login/login.module'; import { MyAccountV2UserModule } from './my-account-v2-user'; -import { OneTimePasswordLoginFormModeule } from './otp-login-form'; +import { OneTimePasswordLoginFormModeule } from './otp-login-form/otp-login-form.module'; +import { VerificationTokenFormModule } from './verification-token-form/verification-token-form.module'; @NgModule({ imports: [ diff --git a/feature-libs/user/account/core/connectors/user-account.connector.spec.ts b/feature-libs/user/account/core/connectors/user-account.connector.spec.ts index bcfaa0e2e6a..019f3ebc438 100644 --- a/feature-libs/user/account/core/connectors/user-account.connector.spec.ts +++ b/feature-libs/user/account/core/connectors/user-account.connector.spec.ts @@ -7,7 +7,7 @@ import createSpy = jasmine.createSpy; const form: LoginForm = { purpose: 'LOGIN', - loginId: 'mockEmail', + loginId: 'test@email.com', password: '1234', }; diff --git a/feature-libs/user/account/core/facade/verification-token.service.spec.ts b/feature-libs/user/account/core/facade/verification-token.service.spec.ts index d1b114b57f2..37f99be929a 100644 --- a/feature-libs/user/account/core/facade/verification-token.service.spec.ts +++ b/feature-libs/user/account/core/facade/verification-token.service.spec.ts @@ -10,7 +10,7 @@ import createSpy = jasmine.createSpy; const form: LoginForm = { purpose: 'LOGIN', - loginId: 'mockEmail', + loginId: 'test@email.com', password: '1234', }; diff --git a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts index 2cac5c49d93..ee8a6777581 100644 --- a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts +++ b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts @@ -63,7 +63,7 @@ const user: User = { const form: LoginForm = { purpose: 'LOGIN', - loginId: 'mockEmail', + loginId: 'test@email.com', password: password, }; @@ -147,10 +147,7 @@ describe('OccUserAccountAdapter', () => { }); expect(occEndpointsService.buildUrl).toHaveBeenCalledWith( - 'createVerificationToken', - { - state: { form }, - } + 'createVerificationToken' ); expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); From 97a40db2f7388a73129d7f508fb733d0700dbca5 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Fri, 19 Apr 2024 17:58:05 +0800 Subject: [PATCH 18/66] CXSPA-6689: add unit tests --- ...erification-token-dialog.component.spec.ts | 64 ++++++++ ...ation-token-form-component.service.spec.ts | 108 +++++++++++++ ...rification-token-form-component.service.ts | 4 +- .../verification-token-form.component.spec.ts | 148 ++++++++++++++++++ .../verification-token-form.component.ts | 12 +- 5 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts create mode 100644 feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts create mode 100644 feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts new file mode 100644 index 00000000000..e8ec2a19227 --- /dev/null +++ b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts @@ -0,0 +1,64 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { FocusDirective, LaunchDialogService } from '@spartacus/storefront'; +import { + VERIFICATION_TOKEN_DIALOG_ACTION, + VerificationTokenDialogComponent, +} from './verification-token-dialog.component'; + +@Pipe({ + name: 'cxTranslate', +}) +class MockTranslatePipe implements PipeTransform { + transform(): any {} +} + +class MockLaunchDialogService implements Partial { + closeDialog(_reason: any) {} +} + +describe('AsmBindCartDialogComponent', () => { + let component: VerificationTokenDialogComponent; + let fixture: ComponentFixture; + + let launchDialogService: LaunchDialogService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + VerificationTokenDialogComponent, + MockTranslatePipe, + FocusDirective, + ], + providers: [ + { provide: LaunchDialogService, useClass: MockLaunchDialogService }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VerificationTokenDialogComponent); + component = fixture.componentInstance; + + launchDialogService = TestBed.inject(LaunchDialogService); + + spyOn(launchDialogService, 'closeDialog').and.stub(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should close with replace action when replace button is clicked', () => { + fixture.detectChanges(); + + fixture.debugElement + .query(By.css('.btn-primary')) + .triggerEventHandler('click'); + + expect(launchDialogService.closeDialog).toHaveBeenCalledWith( + VERIFICATION_TOKEN_DIALOG_ACTION.OK + ); + }); +}); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts new file mode 100644 index 00000000000..00db503105b --- /dev/null +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts @@ -0,0 +1,108 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { + AuthService, + GlobalMessageService, + I18nTestingModule, +} from '@spartacus/core'; +import { FormErrorsModule } from '@spartacus/storefront'; +import { of } from 'rxjs'; +import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; +import createSpy = jasmine.createSpy; + +class MockAuthService implements Partial { + otpLoginWithCredentials = createSpy().and.returnValue(of({})); + isUserLoggedIn = createSpy().and.returnValue(of(true)); +} + +class MockGlobalMessageService { + add = createSpy().and.stub(); + remove = createSpy().and.stub(); +} + +describe('VerificationTokenFormComponentService', () => { + let service: VerificationTokenFormComponentService; + let authService: AuthService; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + FormErrorsModule, + ], + declarations: [], + providers: [ + VerificationTokenFormComponentService, + { provide: AuthService, useClass: MockAuthService }, + { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + service = TestBed.inject(VerificationTokenFormComponentService); + authService = TestBed.inject(AuthService); + }); + + it('should create service', () => { + expect(service).toBeTruthy(); + }); + + describe('login', () => { + const tokenId = ''; + const tokenCode = 'XG5tyu'; + + it('should not patch token id', () => { + service.isUpdating$.subscribe().unsubscribe(); + expect(service.form.value.tokenId).toEqual(''); + }); + + describe('success', () => { + beforeEach(() => { + service.form.setValue({ + tokenId, + tokenCode, + }); + }); + + it('should request email', () => { + service.login(); + expect(authService.otpLoginWithCredentials).toHaveBeenCalledWith( + tokenId, + tokenCode + ); + }); + + it('should reset the form', () => { + spyOn(service.form, 'reset').and.stub(); + service.login(); + expect(service.form.reset).toHaveBeenCalled(); + }); + }); + + describe('error', () => { + beforeEach(() => { + service.form.setValue({ + tokenCode: '', + tokenId: '', + }); + }); + + it('should not login', () => { + service.login(); + expect(authService.otpLoginWithCredentials).not.toHaveBeenCalled(); + }); + + it('should not reset the form', () => { + spyOn(service.form, 'reset').and.stub(); + service.login(); + expect(service.form.reset).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts index b190740e6f9..748bee81238 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts @@ -14,7 +14,6 @@ import { AuthService, GlobalMessageService, GlobalMessageType, - WindowRef, } from '@spartacus/core'; import { VerificationTokenFacade } from '@spartacus/user/account/root'; import { BehaviorSubject, from } from 'rxjs'; @@ -25,8 +24,7 @@ export class VerificationTokenFormComponentService { constructor( protected auth: AuthService, protected globalMessage: GlobalMessageService, - protected verificationTokenFacade: VerificationTokenFacade, - protected winRef: WindowRef + protected verificationTokenFacade: VerificationTokenFacade ) {} protected busy$ = new BehaviorSubject(false); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts new file mode 100644 index 00000000000..25026e43224 --- /dev/null +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts @@ -0,0 +1,148 @@ +import { DebugElement, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { I18nTestingModule } from '@spartacus/core'; +import { + FormErrorsModule, + LaunchDialogService, + SpinnerModule, +} from '@spartacus/storefront'; +import { BehaviorSubject } from 'rxjs'; +import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; +import { VerificationTokenFormComponent } from './verification-token-form.component'; +import createSpy = jasmine.createSpy; + +const isBusySubject = new BehaviorSubject(false); +class MockFormComponentService + implements Partial +{ + form: UntypedFormGroup = new UntypedFormGroup({ + tokenId: new UntypedFormControl(), + tokenCode: new UntypedFormControl(), + }); + isUpdating$ = isBusySubject; + login = createSpy().and.stub(); + sentOTP = createSpy().and.stub(); + displayMessage = createSpy('displayMessage').and.stub(); +} +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} + +class MockLaunchDialogService implements Partial { + openDialogAndSubscribe = createSpy().and.stub(); +} + +describe('VerificationTokenFormComponent', () => { + let component: VerificationTokenFormComponent; + let fixture: ComponentFixture; + let el: DebugElement; + let service: VerificationTokenFormComponentService; + let launchDialogService: LaunchDialogService; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + FormErrorsModule, + SpinnerModule, + ], + declarations: [VerificationTokenFormComponent, MockUrlPipe], + providers: [ + { + provide: VerificationTokenFormComponentService, + useClass: MockFormComponentService, + }, + { + provide: LaunchDialogService, + useClass: MockLaunchDialogService, + }, + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(VerificationTokenFormComponent); + service = TestBed.inject(VerificationTokenFormComponentService); + launchDialogService = TestBed.inject(LaunchDialogService); + component = fixture.componentInstance; + el = fixture.debugElement; + fixture.detectChanges(); + history.pushState( + { + tokenId: '', + password: 'pw4all', + loginId: 'test@sap.com', + }, + '' + ); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + describe('busy', () => { + it('should disable the submit button when form is disabled', () => { + component.form.disable(); + fixture.detectChanges(); + const submitBtn: HTMLButtonElement = el.query( + By.css('button') + ).nativeElement; + expect(submitBtn.disabled).toBeTruthy(); + }); + + it('should show the spinner', () => { + isBusySubject.next(true); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + }); + + describe('idle', () => { + it('should enable the submit button', () => { + component.form.enable(); + fixture.detectChanges(); + const submitBtn = el.query(By.css('button')); + expect(submitBtn.nativeElement.disabled).toBeFalsy(); + }); + + it('should not show the spinner', () => { + isBusySubject.next(false); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeNull(); + }); + }); + + describe('Form Interactions', () => { + it('should call onSubmit() method on submit', () => { + const request = spyOn(component, 'onSubmit'); + const form = el.query(By.css('form')); + form.triggerEventHandler('submit', null); + expect(request).toHaveBeenCalled(); + }); + + it('should call the service method on submit', () => { + component.onSubmit(); + expect(service.login).toHaveBeenCalled(); + }); + + it('should display info dialog', () => { + component.openInfoDailog(); + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + }); + }); +}); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index f6b9a1e4ac8..52c500d6112 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -46,15 +46,17 @@ export class VerificationTokenFormComponent implements OnInit { password: string; - waitTime: int = 60; + waitTime: number = 60; isResendDisabled: boolean = true; ngOnInit() { - this.tokenId = history.state['tokenId']; - this.password = history.state['password']; - this.target = history.state['loginId']; - this.startWaitTimeInterval(); + if (!!history.state) { + this.tokenId = history.state['tokenId']; + this.password = history.state['password']; + this.target = history.state['loginId']; + this.startWaitTimeInterval(); + } } onSubmit(): void { From bfc51650ee81d77b85d3453cc0206ae8b49cc1dd Mon Sep 17 00:00:00 2001 From: niehuayang Date: Mon, 22 Apr 2024 09:47:32 +0800 Subject: [PATCH 19/66] add e2e test --- .../otp-login-form.component.spec.ts | 17 +---- .../otp-login-form.component.ts | 32 ++------- .../assets/src/translations/en/common.json | 3 +- .../user_access/otp-login.e2e-flaky.cy.ts | 65 +++++++++++++++++++ 4 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts index 18a398e0b3b..304d9baac57 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts @@ -3,12 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { - GlobalMessageService, - I18nTestingModule, - RoutingService, - WindowRef, -} from '@spartacus/core'; +import { I18nTestingModule, RoutingService, WindowRef } from '@spartacus/core'; import { FormErrorsModule, SpinnerModule } from '@spartacus/storefront'; import { VerificationTokenService } from '@spartacus/user/account/core'; import { @@ -45,11 +40,6 @@ class MockVerificationTokenService ); } -class MockGlobalMessageService { - add = createSpy().and.stub(); - remove = createSpy().and.stub(); -} - class MockRoutingService implements Partial { go = () => Promise.resolve(true); } @@ -85,7 +75,6 @@ describe('OneTimePasswordLoginFormComponent', () => { useClass: MockVerificationTokenService, }, { provide: WindowRef, useClass: MockWinRef }, - { provide: GlobalMessageService, useClass: MockGlobalMessageService }, { provide: RoutingService, useClass: MockRoutingService }, ], }).compileComponents(); @@ -105,7 +94,7 @@ describe('OneTimePasswordLoginFormComponent', () => { expect(component).toBeTruthy(); }); - describe('login', () => { + describe('create OTP', () => { it('should not patch user id', () => { component.isUpdating$.subscribe().unsubscribe(); expect(component.form.value.userId).toEqual(''); @@ -147,7 +136,7 @@ describe('OneTimePasswordLoginFormComponent', () => { }); }); - it('should not login', () => { + it('should not create OTP', () => { component.onSubmit(); expect(service.createVerificationToken).not.toHaveBeenCalled(); }); diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts index b82f7084b72..2045898f4eb 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.ts @@ -10,13 +10,7 @@ import { UntypedFormGroup, Validators, } from '@angular/forms'; -import { - GlobalMessageService, - GlobalMessageType, - HttpErrorModel, - RoutingService, - WindowRef, -} from '@spartacus/core'; +import { RoutingService, WindowRef } from '@spartacus/core'; import { CustomFormValidators } from '@spartacus/storefront'; import { BehaviorSubject, Observable, tap } from 'rxjs'; @@ -36,8 +30,7 @@ export class OneTimePasswordLoginFormComponent { constructor( protected routingService: RoutingService, protected verificationTokenFacade: VerificationTokenFacade, - protected winRef: WindowRef, - protected globalMessage: GlobalMessageService + protected winRef: WindowRef ) {} protected busy$ = new BehaviorSubject(false); @@ -73,8 +66,7 @@ export class OneTimePasswordLoginFormComponent { this.verificationTokenFacade.createVerificationToken(loginForm).subscribe({ next: (result: VerificationToken) => this.goToVerificationTokenForm(result, loginForm), - error: (error: HttpErrorModel) => - this.onCreateVerificationTokenFail(error), + error: () => this.busy$.next(false), complete: () => this.onCreateVerificationTokenComplete(), }); } @@ -92,28 +84,12 @@ export class OneTimePasswordLoginFormComponent { loginId: loginForm.loginId, password: loginForm.password, tokenId: verificationToken.tokenId, + expiresIn: verificationToken.expiresIn, }, } ); } - protected onCreateVerificationTokenFail(error: HttpErrorModel): void { - this.busy$.next(false); - const errorDetails = error.details ?? []; - if (errorDetails.length === 0) { - this.globalMessage.add( - { key: 'httpHandlers.unknownError' }, - GlobalMessageType.MSG_TYPE_ERROR - ); - } - errorDetails.forEach((err) => { - this.globalMessage.add( - { raw: err.message }, - GlobalMessageType.MSG_TYPE_ERROR - ); - }); - } - protected onCreateVerificationTokenComplete(): void { this.form.reset(); this.busy$.next(false); diff --git a/projects/assets/src/translations/en/common.json b/projects/assets/src/translations/en/common.json index cfc837d9428..3d5452257e6 100644 --- a/projects/assets/src/translations/en/common.json +++ b/projects/assets/src/translations/en/common.json @@ -132,7 +132,8 @@ "password": "Password entered is not valid.", "uid": "UID is not valid.", "code": "Code is not valid.", - "email": "Email is not valid." + "email": "Email is not valid.", + "loginId": "Email is not valid." } }, "cartNotFound": "Cart not found.", diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts new file mode 100644 index 00000000000..2ec343fad50 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as login from '../../../helpers/login'; +import { viewportContext } from '../../../helpers/viewport-context'; +import { interceptPost } from '../../../support/utils/intercept'; + +export function listenForCreateVerificationToken(): string { + return interceptPost( + 'createVerificationToken', + 'users/anonymous/verificationToken' + ); +} + +describe('OTP Login', () => { + viewportContext(['mobile'], () => { + before(() => { + cy.visit('/login'); + }); + + describe('Create OTP', () => { + it('should be able to create a new OTP by customer click Sign In button (CXSPA-6672)', () => { + const user = login.registerUserFromLoginPage(); + listenForCreateVerificationToken(); + + cy.log(`đź›’ Logging in user ${user.email} from the login form`); + cy.get('cx-otp-login-form form').within(() => { + cy.get('[formcontrolname="userId"]').clear().type(user.email); + cy.get('[formcontrolname="password"]').clear().type(user.password); + cy.get('button[type=submit]').click(); + }); + // cy.wait('@createVerificationToken') + // .its('response.statusCode') + // .should('eq', 201); + + cy.get('cx-verification-token-form').should('exist'); + cy.get('cx-verification-token-form').should('be.visible'); + cy.get(login.userGreetSelector).should('not.exist'); + }); + }); + + describe('Failed to Create OTP', () => { + it('should be not able to create OTP with invalid user data (CXSPA-6672)', () => { + listenForCreateVerificationToken(); + cy.get('cx-otp-login-form form').within(() => { + cy.get('[formcontrolname="userId"]') + .clear() + .type('test.user@sap.coma'); + cy.get('[formcontrolname="password"]').clear().type('1234'); + cy.get('button[type=submit]').click(); + }); + // cy.wait('@createVerificationToken') + // .its('response.statusCode') + // .should('eq', 400); + + cy.get('cx-global-message').within(() => { + cy.get('span').contains('Email is not valid.'); + }); + }); + }); + }); +}); From 12d33624bdeea0e5d37fcae2750a08e7ef0c2c17 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Mon, 22 Apr 2024 16:56:09 +0800 Subject: [PATCH 20/66] CXSPA-6689: code refined --- .../verification-token-form.component.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index 52c500d6112..4054dca2acd 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -55,6 +55,14 @@ export class VerificationTokenFormComponent implements OnInit { this.tokenId = history.state['tokenId']; this.password = history.state['password']; this.target = history.state['loginId']; + history.pushState( + { + tokenId: '', + password: '', + loginId: '', + }, + 'verifyToken' + ); this.startWaitTimeInterval(); } } From a8cc45b9235fd9bd03adf695a59d06036b9b2c4b Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Mon, 22 Apr 2024 17:31:37 +0800 Subject: [PATCH 21/66] CXSPA-6689: add unit test --- .../user-auth/facade/auth.service.spec.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/projects/core/src/auth/user-auth/facade/auth.service.spec.ts b/projects/core/src/auth/user-auth/facade/auth.service.spec.ts index 74d79f03d93..5ae04fa06f4 100644 --- a/projects/core/src/auth/user-auth/facade/auth.service.spec.ts +++ b/projects/core/src/auth/user-auth/facade/auth.service.spec.ts @@ -185,6 +185,30 @@ describe('AuthService', () => { }); }); + describe('otpLoginWithCredentials()', () => { + it('should login user', async () => { + spyOn( + oAuthLibWrapperService, + 'authorizeWithPasswordFlow' + ).and.callThrough(); + spyOn(userIdService, 'setUserId').and.callThrough(); + spyOn(authRedirectService, 'redirect').and.callThrough(); + spyOn(store, 'dispatch').and.callThrough(); + + const tokenId = ''; + const tokenCode = 'XD2iuP'; + + await service.otpLoginWithCredentials(tokenId, tokenCode); + + expect( + oAuthLibWrapperService.authorizeWithPasswordFlow + ).toHaveBeenCalledWith(tokenId, tokenCode); + expect(userIdService.setUserId).toHaveBeenCalledWith(OCC_USER_ID_CURRENT); + expect(store.dispatch).toHaveBeenCalledWith(new AuthActions.Login()); + expect(authRedirectService.redirect).toHaveBeenCalled(); + }); + }); + describe('coreLogout()', () => { it('should revoke tokens and logout', fakeAsync(() => { spyOn(userIdService, 'clearUserId').and.callThrough(); From d11f38375391dff3046659feb330ae69452b7f5c Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Mon, 22 Apr 2024 17:55:38 +0800 Subject: [PATCH 22/66] CXSPA: sonar fixed --- .../assets/translations/en/userAccount.json | 2 +- .../verification-token-dialog.component.ts | 6 ++---- .../verification-token-form.component.ts | 2 +- .../verification-token-form.module.ts | 7 ++++++- .../account/core/connectors/converters.ts | 20 ++++++++++--------- feature-libs/user/account/styles/_index.scss | 1 - .../styles/_verification-token-form.scss | 4 ++-- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/feature-libs/user/account/assets/translations/en/userAccount.json b/feature-libs/user/account/assets/translations/en/userAccount.json index e109c12a4c0..407ea159b5e 100644 --- a/feature-libs/user/account/assets/translations/en/userAccount.json +++ b/feature-libs/user/account/assets/translations/en/userAccount.json @@ -42,4 +42,4 @@ "myAccountV2User": { "signOut": "Sign Out" } -} \ No newline at end of file +} diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts index 55b98f67545..7a32f089c5e 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { FocusConfig, LaunchDialogService } from '@spartacus/storefront'; export enum VERIFICATION_TOKEN_DIALOG_ACTION { @@ -15,7 +15,7 @@ export enum VERIFICATION_TOKEN_DIALOG_ACTION { selector: 'cx-verification-token-dialog', templateUrl: './verification-token-dialog.component.html', }) -export class VerificationTokenDialogComponent implements OnInit { +export class VerificationTokenDialogComponent { VERIFICATION_TOKEN_DIALOG_ACTION = VERIFICATION_TOKEN_DIALOG_ACTION; focusConfig: FocusConfig = { @@ -27,8 +27,6 @@ export class VerificationTokenDialogComponent implements OnInit { constructor(protected launchDialogService: LaunchDialogService) {} - ngOnInit(): void {} - closeModal(reason: VERIFICATION_TOKEN_DIALOG_ACTION): void { this.launchDialogService.closeDialog(reason); } diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index 4054dca2acd..fae0e43f497 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -84,7 +84,7 @@ export class VerificationTokenFormComponent implements OnInit { } startWaitTimeInterval(): void { - let interval = setInterval(() => { + const interval = setInterval(() => { this.waitTime--; this.cdr.detectChanges(); if (this.waitTime <= 0) { diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts index 1fc2f50d6ad..6a332aa2507 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts @@ -55,7 +55,12 @@ import { VerificationTokenFacade } from '../../root/facade'; { provide: VerificationTokenFormComponentService, useClass: VerificationTokenFormComponentService, - deps: [AuthService, GlobalMessageService, VerificationTokenFacade, WindowRef], + deps: [ + AuthService, + GlobalMessageService, + VerificationTokenFacade, + WindowRef, + ], }, ], }, diff --git a/feature-libs/user/account/core/connectors/converters.ts b/feature-libs/user/account/core/connectors/converters.ts index 163a5c2c131..434cf2c151f 100644 --- a/feature-libs/user/account/core/connectors/converters.ts +++ b/feature-libs/user/account/core/connectors/converters.ts @@ -6,7 +6,11 @@ import { InjectionToken } from '@angular/core'; import { Converter } from '@spartacus/core'; -import { LoginForm, User, VerificationToken } from '@spartacus/user/account/root'; +import { + LoginForm, + User, + VerificationToken, +} from '@spartacus/user/account/root'; export const USER_ACCOUNT_NORMALIZER = new InjectionToken>( 'UserAccountNormalizer' @@ -16,12 +20,10 @@ export const USER_ACCOUNT_SERIALIZER = new InjectionToken>( 'UserAccountSerializer' ); +export const VERIFICATION_TOKEN_NORMALIZER = new InjectionToken< + Converter +>('VerificationTokenNormalizer'); -export const VERIFICATION_TOKEN_NORMALIZER = new InjectionToken>( - 'VerificationTokenNormalizer' -); - -export const LOGIN_FORM_SERIALIZER = new InjectionToken>( - 'LoginFormSerializer' -); - +export const LOGIN_FORM_SERIALIZER = new InjectionToken< + Converter +>('LoginFormSerializer'); diff --git a/feature-libs/user/account/styles/_index.scss b/feature-libs/user/account/styles/_index.scss index 6adb0b7c7a6..ab2814ca398 100644 --- a/feature-libs/user/account/styles/_index.scss +++ b/feature-libs/user/account/styles/_index.scss @@ -2,6 +2,5 @@ @import './login-form'; @import './otp-login-form'; @import './my-account-v2-user'; -@import './otp-login-form'; @import './verification-token-form'; @import './verification-token-dialog'; diff --git a/feature-libs/user/account/styles/_verification-token-form.scss b/feature-libs/user/account/styles/_verification-token-form.scss index 294067da99d..f846dd89fcc 100644 --- a/feature-libs/user/account/styles/_verification-token-form.scss +++ b/feature-libs/user/account/styles/_verification-token-form.scss @@ -6,12 +6,12 @@ .left-text { padding: 0; - text-align: left; + text-align: var(--cx-text-align, left); } .right-text { padding: 0; - text-align: right; + text-align: var(--cx-text-align, right); } a.disabled-link { From cb9b70bf617f020fd931a819a87f620fb591a424 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Tue, 23 Apr 2024 13:33:54 +0800 Subject: [PATCH 23/66] CXSPA-6689: add unit tests --- .../verification-token-form.component.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts index 25026e43224..1a987f736f2 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts @@ -144,5 +144,16 @@ describe('VerificationTokenFormComponent', () => { component.openInfoDailog(); expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); }); + + it('should resend otp token', () => { + component.resendOTP(); + expect(service.sentOTP).toHaveBeenCalled(); + }); + + it('should resend otp token', () => { + component.resendOTP(); + expect(service.sentOTP).toHaveBeenCalled(); + expect(service.displayMessage).toHaveBeenCalled(); + }); }); }); From f4ef714d10309220b4b3f32d9258602a38cea8ea Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Tue, 23 Apr 2024 16:27:55 +0800 Subject: [PATCH 24/66] CXSPA-6689: code refined --- ...ation-token-form-component.service.spec.ts | 26 +++++++++++++ ...rification-token-form-component.service.ts | 2 +- .../verification-token-form.component.spec.ts | 38 ++++++++++++++----- .../verification-token-form.component.ts | 13 ++++--- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts index 00db503105b..6f34a716e59 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts @@ -8,6 +8,7 @@ import { } from '@spartacus/core'; import { FormErrorsModule } from '@spartacus/storefront'; import { of } from 'rxjs'; +import { VerificationTokenFacade } from '../../root/facade'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; import createSpy = jasmine.createSpy; @@ -16,6 +17,12 @@ class MockAuthService implements Partial { isUserLoggedIn = createSpy().and.returnValue(of(true)); } +class MockVerificationTokenFacade implements Partial { + createVerificationToken = createSpy().and.returnValue( + of({ tokenId: 'testTokenId', expiresIn: '300' }) + ); +} + class MockGlobalMessageService { add = createSpy().and.stub(); remove = createSpy().and.stub(); @@ -24,6 +31,7 @@ class MockGlobalMessageService { describe('VerificationTokenFormComponentService', () => { let service: VerificationTokenFormComponentService; let authService: AuthService; + let facade: VerificationTokenFacade; beforeEach( waitForAsync(() => { @@ -39,6 +47,10 @@ describe('VerificationTokenFormComponentService', () => { VerificationTokenFormComponentService, { provide: AuthService, useClass: MockAuthService }, { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + { + provide: VerificationTokenFacade, + useClass: MockVerificationTokenFacade, + }, ], }).compileComponents(); }) @@ -47,12 +59,26 @@ describe('VerificationTokenFormComponentService', () => { beforeEach(() => { service = TestBed.inject(VerificationTokenFormComponentService); authService = TestBed.inject(AuthService); + facade = TestBed.inject(VerificationTokenFacade); }); it('should create service', () => { expect(service).toBeTruthy(); }); + it('should sent otp', () => { + const loginId = 'example@example.com'; + const password = 'pw4all'; + const purpose = 'LOGIN'; + + service.sentOTP(loginId, password, purpose); + expect(facade.createVerificationToken).toHaveBeenCalledWith({ + loginId, + password, + purpose, + }); + }); + describe('login', () => { const tokenId = ''; const tokenCode = 'XG5tyu'; diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts index 748bee81238..1333c17df3e 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.ts @@ -72,7 +72,7 @@ export class VerificationTokenFormComponentService { } sentOTP(loginId: string, password: string, purpose: string) { - this.verificationTokenFacade.createVerificationToken({ + return this.verificationTokenFacade.createVerificationToken({ loginId, password, purpose, diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts index 1a987f736f2..a5f27eb29e1 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts @@ -1,4 +1,9 @@ -import { DebugElement, Pipe, PipeTransform } from '@angular/core'; +import { + ChangeDetectorRef, + DebugElement, + Pipe, + PipeTransform, +} from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ReactiveFormsModule, @@ -13,7 +18,8 @@ import { LaunchDialogService, SpinnerModule, } from '@spartacus/storefront'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; +import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from '../user-account-constants'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; import { VerificationTokenFormComponent } from './verification-token-form.component'; import createSpy = jasmine.createSpy; @@ -28,7 +34,9 @@ class MockFormComponentService }); isUpdating$ = isBusySubject; login = createSpy().and.stub(); - sentOTP = createSpy().and.stub(); + sentOTP = createSpy().and.returnValue( + of({ tokenId: 'testTokenId', expiresIn: '300' }) + ); displayMessage = createSpy('displayMessage').and.stub(); } @Pipe({ @@ -69,6 +77,7 @@ describe('VerificationTokenFormComponent', () => { provide: LaunchDialogService, useClass: MockLaunchDialogService, }, + ChangeDetectorRef, ], }).compileComponents(); }) @@ -145,15 +154,24 @@ describe('VerificationTokenFormComponent', () => { expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); }); - it('should resend otp token', () => { - component.resendOTP(); - expect(service.sentOTP).toHaveBeenCalled(); - }); + it('should resend OTP', () => { + component.target = 'example@example.com'; + component.password = 'password'; + spyOn(component, 'startWaitTimeInterval'); - it('should resend otp token', () => { component.resendOTP(); - expect(service.sentOTP).toHaveBeenCalled(); - expect(service.displayMessage).toHaveBeenCalled(); + + expect(component.isResendDisabled).toBe(true); + expect(component.waitTime).toBe(60); + expect(component.startWaitTimeInterval).toHaveBeenCalled(); + expect(service.sentOTP).toHaveBeenCalledWith( + 'example@example.com', + 'password', + ONE_TIME_PASSWORD_LOGIN_PURPOSE + ); + expect(service.displayMessage).toHaveBeenCalledWith( + 'example@example.com' + ); }); }); }); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index fae0e43f497..1e3c659b3a4 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -16,6 +16,7 @@ import { import { UntypedFormGroup } from '@angular/forms'; import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; import { Observable } from 'rxjs'; +import { VerificationToken } from '../../root/model'; import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from '../user-account-constants'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; @@ -75,12 +76,12 @@ export class VerificationTokenFormComponent implements OnInit { this.isResendDisabled = true; this.waitTime = 60; this.startWaitTimeInterval(); - this.service.sentOTP( - this.target, - this.password, - ONE_TIME_PASSWORD_LOGIN_PURPOSE - ); - this.service.displayMessage(this.target); + this.service + .sentOTP(this.target, this.password, ONE_TIME_PASSWORD_LOGIN_PURPOSE) + .subscribe({ + next: (result: VerificationToken) => (this.tokenId = result.tokenId), + complete: () => this.service.displayMessage(this.target), + }); } startWaitTimeInterval(): void { From 05b20411f9e626aead19dc9f9ffdf335278de623 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Tue, 23 Apr 2024 16:47:23 +0800 Subject: [PATCH 25/66] CXSPA-6689: bug fixed --- .../verification-token-form.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index 1e3c659b3a4..d38d16c046b 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -64,8 +64,9 @@ export class VerificationTokenFormComponent implements OnInit { }, 'verifyToken' ); - this.startWaitTimeInterval(); } + this.startWaitTimeInterval(); + this.service.displayMessage(this.target); } onSubmit(): void { From 8ef7f268a5a0a2cc4f7ce4204461fedc2ee3660c Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Tue, 23 Apr 2024 17:36:49 +0800 Subject: [PATCH 26/66] CXSPA-6689: code refined --- projects/core/src/auth/user-auth/facade/auth.service.ts | 1 - .../core/src/auth/user-auth/services/auth-redirect.service.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/projects/core/src/auth/user-auth/facade/auth.service.ts b/projects/core/src/auth/user-auth/facade/auth.service.ts index eb4a3e56baa..8d75fe86f49 100644 --- a/projects/core/src/auth/user-auth/facade/auth.service.ts +++ b/projects/core/src/auth/user-auth/facade/auth.service.ts @@ -129,7 +129,6 @@ export class AuthService { this.store.dispatch(new AuthActions.Login()); - debugger; this.authRedirectService.redirect(); } catch {} } diff --git a/projects/core/src/auth/user-auth/services/auth-redirect.service.ts b/projects/core/src/auth/user-auth/services/auth-redirect.service.ts index 587f0342ac3..09829e8173b 100644 --- a/projects/core/src/auth/user-auth/services/auth-redirect.service.ts +++ b/projects/core/src/auth/user-auth/services/auth-redirect.service.ts @@ -65,10 +65,8 @@ export class AuthRedirectService implements OnDestroy { .pipe(take(1)) .subscribe((redirectUrl) => { if (redirectUrl === undefined) { - debugger; this.routing.go('/'); } else { - debugger; this.routing.goByUrl(redirectUrl); } this.clearRedirectUrl(); From 0b5764694260152e924ec90798e85299d05c5f16 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Wed, 24 Apr 2024 10:19:12 +0800 Subject: [PATCH 27/66] refine lighthouse check --- .../verification-token-form/verification-token-form.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts index 6a332aa2507..38dee64cb86 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.module.ts @@ -25,11 +25,12 @@ import { KeyboardFocusModule, SpinnerModule, } from '@spartacus/storefront'; + import { defaultVerificationTokenLayoutConfig } from './default-verification-token-layout.config'; import { VerificationTokenDialogComponent } from './verification-token-dialog.component'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; import { VerificationTokenFormComponent } from './verification-token-form.component'; -import { VerificationTokenFacade } from '../../root/facade'; +import { VerificationTokenFacade } from '@spartacus/user/account/root'; @NgModule({ imports: [ From 9ad0965dc443b75591a0f4c80a5f9c602ac10d86 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Wed, 24 Apr 2024 11:04:55 +0800 Subject: [PATCH 28/66] refine code --- .../verification-token-form.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts index d38d16c046b..6782e15af21 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.ts @@ -16,9 +16,10 @@ import { import { UntypedFormGroup } from '@angular/forms'; import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; import { Observable } from 'rxjs'; -import { VerificationToken } from '../../root/model'; + import { ONE_TIME_PASSWORD_LOGIN_PURPOSE } from '../user-account-constants'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; +import { VerificationToken } from '@spartacus/user/account/root'; @Component({ selector: 'cx-verification-token-form', From 33eb70ff12287fe4f73b00813433d761e99ec7e8 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Wed, 24 Apr 2024 11:10:14 +0800 Subject: [PATCH 29/66] CXSPA-6689: code refined --- .../model/{augmented-core.model.ts => augmented.model.ts} | 0 feature-libs/user/account/root/model/index.ts | 2 +- .../removed-public-api-deprecation.ts | 3 +-- .../src/migrations/4_0/rename-symbol/rename-symbol.ts | 7 ------- 4 files changed, 2 insertions(+), 10 deletions(-) rename feature-libs/user/account/root/model/{augmented-core.model.ts => augmented.model.ts} (100%) diff --git a/feature-libs/user/account/root/model/augmented-core.model.ts b/feature-libs/user/account/root/model/augmented.model.ts similarity index 100% rename from feature-libs/user/account/root/model/augmented-core.model.ts rename to feature-libs/user/account/root/model/augmented.model.ts diff --git a/feature-libs/user/account/root/model/index.ts b/feature-libs/user/account/root/model/index.ts index beafee30f70..d0ddad8c795 100644 --- a/feature-libs/user/account/root/model/index.ts +++ b/feature-libs/user/account/root/model/index.ts @@ -4,6 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './augmented-core.model'; +export * from './augmented.model'; export * from './otp-login.model'; export * from './user.model'; diff --git a/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts b/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts index fdfababa283..63a607f8c3d 100644 --- a/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts +++ b/projects/schematics/src/migrations/4_0/removed-public-api-deprecations/removed-public-api-deprecation.ts @@ -47,7 +47,6 @@ import { OCC_USER_ACCOUNT_ADAPTER, OCC_USER_ADAPTER, OCC_USER_PROFILE_ADAPTER, - OTP_LOGIN_FORM_MODULE, PAGE_EVENT_BUILDER, PAGE_EVENT_MODULE, PRODUCT_VARIANTS_MODULE, @@ -370,7 +369,7 @@ export const REMOVED_PUBLIC_API_DATA: DeprecatedNode[] = [ { node: USER_COMPONENT_MODULE, importPath: SPARTACUS_STOREFRONTLIB, - comment: `'${USER_COMPONENT_MODULE}' - Following module imports '${LOGIN_MODULE}', '${LOGIN_FORM_MODULE}','${OTP_LOGIN_FORM_MODULE}', '${LOGIN_REGISTER_MODULE}', '${REGISTER_COMPONENT_MODULE}' were removed. Those modules are now part of ${SPARTACUS_USER}.`, + comment: `'${USER_COMPONENT_MODULE}' - Following module imports '${LOGIN_MODULE}', '${LOGIN_FORM_MODULE}', '${LOGIN_REGISTER_MODULE}', '${REGISTER_COMPONENT_MODULE}' were removed. Those modules are now part of ${SPARTACUS_USER}.`, }, // projects/storefrontlib/cms-components/myaccount/close-account/components/close-account-modal/close-account-modal.component.ts { diff --git a/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts b/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts index 01a1f5a5266..a0e599ff716 100644 --- a/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts +++ b/projects/schematics/src/migrations/4_0/rename-symbol/rename-symbol.ts @@ -44,7 +44,6 @@ import { LOGIN_REGISTER_MODULE, OCC_ASM_ADAPTER, ORDER_ENTRY, - OTP_LOGIN_FORM_MODULE, PERMISSION_ROUTING_CONFIG, PERSONALIZATION_ACTION, PERSONALIZATION_CONFIG, @@ -419,12 +418,6 @@ export const RENAMED_SYMBOLS_DATA: RenamedSymbol[] = [ previousImportPath: SPARTACUS_STOREFRONTLIB, newImportPath: SPARTACUS_USER_ACCOUNT_COMPONENTS, }, - // projects/storefrontlib/cms-components/user/otp-login-form/otp-login-form.module.ts - { - previousNode: OTP_LOGIN_FORM_MODULE, - previousImportPath: SPARTACUS_STOREFRONTLIB, - newImportPath: SPARTACUS_USER_ACCOUNT_COMPONENTS, - }, // projects/storefrontlib/cms-components/user/login-register/login-register.module.ts { previousNode: LOGIN_REGISTER_MODULE, From 5421af99625df8a262cc42456fa6c34c96de3e52 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Wed, 24 Apr 2024 11:38:11 +0800 Subject: [PATCH 30/66] add e2e test --- .../core/facade/verification-token.service.ts | 7 +- .../user_access/otp-login.e2e-flaky.cy.ts | 90 ++++++++++++++++--- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/feature-libs/user/account/core/facade/verification-token.service.ts b/feature-libs/user/account/core/facade/verification-token.service.ts index 6238ae2ade9..f7331b24cc1 100644 --- a/feature-libs/user/account/core/facade/verification-token.service.ts +++ b/feature-libs/user/account/core/facade/verification-token.service.ts @@ -6,9 +6,12 @@ import { Injectable } from '@angular/core'; import { Command, CommandService } from '@spartacus/core'; +import { + LoginForm, + VerificationToken, + VerificationTokenFacade, +} from '@spartacus/user/account/root'; import { Observable } from 'rxjs'; -import { VerificationTokenFacade } from '../../root/facade'; -import { LoginForm, VerificationToken } from '../../root/model'; import { UserAccountConnector } from '../connectors'; @Injectable() diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts index 2ec343fad50..bc1c54ce623 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts @@ -11,17 +11,17 @@ import { interceptPost } from '../../../support/utils/intercept'; export function listenForCreateVerificationToken(): string { return interceptPost( 'createVerificationToken', - 'users/anonymous/verificationToken' + '/users/anonymous/verificationToken?*' ); } describe('OTP Login', () => { viewportContext(['mobile'], () => { - before(() => { - cy.visit('/login'); - }); - describe('Create OTP', () => { + beforeEach(() => { + cy.visit('/login'); + }); + it('should be able to create a new OTP by customer click Sign In button (CXSPA-6672)', () => { const user = login.registerUserFromLoginPage(); listenForCreateVerificationToken(); @@ -32,19 +32,69 @@ describe('OTP Login', () => { cy.get('[formcontrolname="password"]').clear().type(user.password); cy.get('button[type=submit]').click(); }); - // cy.wait('@createVerificationToken') - // .its('response.statusCode') - // .should('eq', 201); + + cy.wait('@createVerificationToken') + .its('response.statusCode') + .should('eq', 201); cy.get('cx-verification-token-form').should('exist'); cy.get('cx-verification-token-form').should('be.visible'); - cy.get(login.userGreetSelector).should('not.exist'); + + cy.log('The email being sent is asynchronous, so waiting 10s'); + cy.wait(10000); + + const API_ENDPOINT = + 'http://mail-ccv2.westeurope.azurecontainer.io:8025/api/v2/search'; + cy.request({ + method: 'GET', + url: + API_ENDPOINT + '?query=' + user.email + '&kind=to&start=0&limit=2', + }).then((response) => { + const subject = + '[Spartacus Electronics Site] Login Verification Code'; + const verificationCodeEmailStartText = + 'Please use the following verification code to log in Spartacus Electronics Site:

'; + const lableP = '

'; + + const items = response.body.items; + const emailBody = + subject === items[0].Content.Headers.Subject[0] + ? items[0].Content.Body + : items[1].Content.Body; + + const verificationCodeEmailStartIndex = + emailBody.indexOf(verificationCodeEmailStartText) + + verificationCodeEmailStartText.length; + const verificationCodeStartIndex = + emailBody.indexOf(lableP, verificationCodeEmailStartIndex) + + lableP.length; + const verificationCode = emailBody.substring( + verificationCodeStartIndex, + verificationCodeStartIndex + 8 + ); + cy.log('Extracted verification code: ' + verificationCode); + + login.listenForTokenAuthenticationRequest(); + cy.get('cx-verification-token-form form').within(() => { + cy.get('[formcontrolname="tokenCode"]') + .clear() + .type(verificationCode); + cy.get('button[type=submit]').click(); + }); + cy.wait('@tokenAuthentication') + .its('response.statusCode') + .should('eq', 200); + }); }); }); describe('Failed to Create OTP', () => { + beforeEach(() => { + cy.visit('/login'); + }); it('should be not able to create OTP with invalid user data (CXSPA-6672)', () => { listenForCreateVerificationToken(); + cy.get('cx-otp-login-form form').within(() => { cy.get('[formcontrolname="userId"]') .clear() @@ -52,14 +102,30 @@ describe('OTP Login', () => { cy.get('[formcontrolname="password"]').clear().type('1234'); cy.get('button[type=submit]').click(); }); - // cy.wait('@createVerificationToken') - // .its('response.statusCode') - // .should('eq', 400); + + cy.wait('@createVerificationToken') + .its('response.statusCode') + .should('eq', 400); cy.get('cx-global-message').within(() => { cy.get('span').contains('Email is not valid.'); }); }); }); + + describe('Chould can return back to login page from verify code page', () => { + it('should be not able to create OTP with invalid user data (CXSPA-6672)', () => { + cy.visit('/login/verify-token'); + + cy.get('cx-verification-token-form').should('exist'); + + cy.get('cx-verification-token-form form').within(() => { + cy.get('.btn-secondary').click(); + }); + + cy.get('cx-verification-token-form').should('not.exist'); + cy.get('cx-otp-login-form form').should('exist'); + }); + }); }); }); From de1c0200a64d2e61e93a67450f3a4fc82ad8df49 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Thu, 25 Apr 2024 11:31:39 +0800 Subject: [PATCH 31/66] add accessibility e2e test --- .../otp-login-tabbing.e2e-spec-flaky.cy.ts | 64 +++++++++++++++++++ .../user_access/otp-login.e2e-flaky.cy.ts | 6 +- .../accessibility/tabbing-order.config.ts | 14 ++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts new file mode 100644 index 00000000000..ecc7b977ecc --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ +import { verifyTabbingOrder } from '../../helpers/accessibility/tabbing-order'; +import { tabbingOrderConfig as config } from '../../helpers/accessibility/tabbing-order.config'; + +describe('Tabbing order for OTP login', () => { + before(() => { + cy.window().then((win) => win.sessionStorage.clear()); + }); + + describe('OTP login', () => { + context('OTP Login page', () => { + beforeEach(() => { + cy.visit('/login'); + cy.get('cx-otp-login-form').should('exist'); + cy.get('cx-otp-login-form form').should('exist'); + }); + it('should allow to navigate with tab key for otp login form(empty form)', () => { + verifyTabbingOrder('cx-otp-login-form', config.otpLogin); + }); + + it('should allow to navigate with tab key for otp login form(filled out form)', () => { + const { email: username, password } = user; + cy.get('cx-otp-login-form form').within(() => { + cy.get('[formcontrolname="userId"]').clear().type(username); + cy.get('[formcontrolname="password"]').clear().type(password); + }); + verifyTabbingOrder('cx-otp-login-form', config.otpLogin); + }); + }); + + // it('should allow to navigate with tab key for otp verification token form', () => { + // cy.visit('/login/verify-token'); + // cy.get('cx-verification-token-form').should('exist'); + // cy.get('cx-verification-token-form form').should('exist'); + + // verifyTabbingOrder( + // 'cx-verification-token-form', + // config.verifyToken + // ); + // }); + + // it('should allow to navigate with tab key for otp verification token dialog', () => { + // cy.visit('/login/verify-token'); + // cy.get('cx-verification-token-form form').within(() => { + // cy.get('.right-text .btn-link') + // .first() + // .contains("Didn't receive the code?") + // .click() + // .then(() => { + // cy.get('cx-verification-token-dialog').should('exist'); + + // verifyTabbingOrder( + // 'cx-verification-token-dialog', + // config.verifyTokenDialog + // ); + // }); + // }); + // }); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts index bc1c54ce623..daaaec25d73 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts @@ -113,13 +113,13 @@ describe('OTP Login', () => { }); }); - describe('Chould can return back to login page from verify code page', () => { - it('should be not able to create OTP with invalid user data (CXSPA-6672)', () => { + describe('Verification token', () => { + it('Should can return back to otp login page from verification token page (CXSPA-6689)', () => { cy.visit('/login/verify-token'); cy.get('cx-verification-token-form').should('exist'); - cy.get('cx-verification-token-form form').within(() => { + cy.get('cx-verification-token-form form a').within(() => { cy.get('.btn-secondary').click(); }); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts index 810cc8bc5d1..b2b527c4cdc 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts @@ -2509,4 +2509,18 @@ export const tabbingOrderConfig: TabbingOrderConfig = { // uncomment the following line to test againt S4,S7 { type: TabbingOrderTypes.BUTTON, value: ' Assign to Customer ' }, ], + otpLogin: [ + { value: 'userId', type: TabbingOrderTypes.FORM_FIELD }, + { value: 'password', type: TabbingOrderTypes.FORM_FIELD }, + { value: '', type: TabbingOrderTypes.LINK }, + { value: 'Forgot password?', type: TabbingOrderTypes.LINK }, + { value: 'Sign In', type: TabbingOrderTypes.BUTTON }, + ], + verifyToken: [ + { value: 'tokenCode', type: TabbingOrderTypes.FORM_FIELD }, + { value: "Didn't receive the code?", type: TabbingOrderTypes.LINK }, + { value: 'Verify', type: TabbingOrderTypes.BUTTON }, + { value: 'Back', type: TabbingOrderTypes.LINK }, + ], + verifyTokenDialog: [{ value: 'ok', type: TabbingOrderTypes.BUTTON }], }; From e697cae511675699239f8e380ff40e354a78d0c6 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Thu, 25 Apr 2024 11:34:49 +0800 Subject: [PATCH 32/66] remove unused code --- .../otp-login-tabbing.e2e-spec-flaky.cy.ts | 30 +------------------ .../accessibility/tabbing-order.config.ts | 7 ----- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts index ecc7b977ecc..22230805eae 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts @@ -5,6 +5,7 @@ */ import { verifyTabbingOrder } from '../../helpers/accessibility/tabbing-order'; import { tabbingOrderConfig as config } from '../../helpers/accessibility/tabbing-order.config'; +import { user } from '../../sample-data/checkout-flow'; describe('Tabbing order for OTP login', () => { before(() => { @@ -31,34 +32,5 @@ describe('Tabbing order for OTP login', () => { verifyTabbingOrder('cx-otp-login-form', config.otpLogin); }); }); - - // it('should allow to navigate with tab key for otp verification token form', () => { - // cy.visit('/login/verify-token'); - // cy.get('cx-verification-token-form').should('exist'); - // cy.get('cx-verification-token-form form').should('exist'); - - // verifyTabbingOrder( - // 'cx-verification-token-form', - // config.verifyToken - // ); - // }); - - // it('should allow to navigate with tab key for otp verification token dialog', () => { - // cy.visit('/login/verify-token'); - // cy.get('cx-verification-token-form form').within(() => { - // cy.get('.right-text .btn-link') - // .first() - // .contains("Didn't receive the code?") - // .click() - // .then(() => { - // cy.get('cx-verification-token-dialog').should('exist'); - - // verifyTabbingOrder( - // 'cx-verification-token-dialog', - // config.verifyTokenDialog - // ); - // }); - // }); - // }); }); }); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts index b2b527c4cdc..79aed683e9c 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts @@ -2516,11 +2516,4 @@ export const tabbingOrderConfig: TabbingOrderConfig = { { value: 'Forgot password?', type: TabbingOrderTypes.LINK }, { value: 'Sign In', type: TabbingOrderTypes.BUTTON }, ], - verifyToken: [ - { value: 'tokenCode', type: TabbingOrderTypes.FORM_FIELD }, - { value: "Didn't receive the code?", type: TabbingOrderTypes.LINK }, - { value: 'Verify', type: TabbingOrderTypes.BUTTON }, - { value: 'Back', type: TabbingOrderTypes.LINK }, - ], - verifyTokenDialog: [{ value: 'ok', type: TabbingOrderTypes.BUTTON }], }; From 8615672029b78f31dea7f67bded6031a5c0f6b69 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Thu, 25 Apr 2024 13:30:03 +0800 Subject: [PATCH 33/66] CXSPA-6689: bug fixed --- .../verification-token-form.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html index 080e1a5723f..6917e916568 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html @@ -22,6 +22,7 @@

From ec2a0adc9c3f2e4bb652b729df51f327cfa9d4a1 Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Thu, 25 Apr 2024 13:39:05 +0800 Subject: [PATCH 34/66] CXSPA-6689: bug fixed --- .../verification-token-form.component.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html index 6917e916568..6068e49dc37 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.html @@ -22,7 +22,7 @@ From 005469b4dfe1f139568271a853f6ff1123589947 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Thu, 25 Apr 2024 14:03:35 +0800 Subject: [PATCH 35/66] refine e2e --- .../accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts | 8 ++++++++ .../e2e/regression/user_access/otp-login.e2e-flaky.cy.ts | 4 ++-- .../cypress/helpers/accessibility/tabbing-order.config.ts | 7 +++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts index 22230805eae..25c46944b24 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-login-tabbing.e2e-spec-flaky.cy.ts @@ -32,5 +32,13 @@ describe('Tabbing order for OTP login', () => { verifyTabbingOrder('cx-otp-login-form', config.otpLogin); }); }); + + it('should allow to navigate with tab key for otp verification token form', () => { + cy.visit('/login/verify-token'); + cy.get('cx-verification-token-form').should('exist'); + cy.get('cx-verification-token-form form').should('exist'); + + verifyTabbingOrder('cx-verification-token-form', config.verifyToken); + }); }); }); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts index daaaec25d73..3f0359bf969 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts @@ -119,8 +119,8 @@ describe('OTP Login', () => { cy.get('cx-verification-token-form').should('exist'); - cy.get('cx-verification-token-form form a').within(() => { - cy.get('.btn-secondary').click(); + cy.get('cx-verification-token-form form').within(() => { + cy.get('div.verify-container a').click(); }); cy.get('cx-verification-token-form').should('not.exist'); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts index 79aed683e9c..93d8467f38c 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order.config.ts @@ -2516,4 +2516,11 @@ export const tabbingOrderConfig: TabbingOrderConfig = { { value: 'Forgot password?', type: TabbingOrderTypes.LINK }, { value: 'Sign In', type: TabbingOrderTypes.BUTTON }, ], + verifyToken: [ + { value: 'tokenCode', type: TabbingOrderTypes.FORM_FIELD }, + { value: 'Resend', type: TabbingOrderTypes.LINK }, + { value: "Didn't receive the code?", type: TabbingOrderTypes.LINK }, + { value: 'Verify', type: TabbingOrderTypes.BUTTON }, + { value: 'Back', type: TabbingOrderTypes.LINK }, + ], }; From d547263bc832a80f5300c594d70cf48a11bfa592 Mon Sep 17 00:00:00 2001 From: niehuayang Date: Thu, 25 Apr 2024 14:25:07 +0800 Subject: [PATCH 36/66] fix comments --- .../e2e/regression/user_access/otp-login.e2e-flaky.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts index 3f0359bf969..9ccda504aa0 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-login.e2e-flaky.cy.ts @@ -92,7 +92,7 @@ describe('OTP Login', () => { beforeEach(() => { cy.visit('/login'); }); - it('should be not able to create OTP with invalid user data (CXSPA-6672)', () => { + it('should not be able to create OTP with invalid user data (CXSPA-6672)', () => { listenForCreateVerificationToken(); cy.get('cx-otp-login-form form').within(() => { @@ -114,7 +114,7 @@ describe('OTP Login', () => { }); describe('Verification token', () => { - it('Should can return back to otp login page from verification token page (CXSPA-6689)', () => { + it('Should go back to login page when click back button (CXSPA-6689)', () => { cy.visit('/login/verify-token'); cy.get('cx-verification-token-form').should('exist'); From f685db2c070de7758b4817e92a63f88021b460ef Mon Sep 17 00:00:00 2001 From: "Ran, Lulu" Date: Thu, 25 Apr 2024 15:40:16 +0800 Subject: [PATCH 37/66] CXSPA: code refined for code review --- .../assets/translations/en/userAccount.json | 4 +- .../verification-token-dialog.component.html | 71 +++++++++---------- ...ation-token-form-component.service.spec.ts | 2 +- ...rification-token-form-component.service.ts | 8 ++- .../verification-token-form.component.spec.ts | 4 +- .../verification-token-form.component.ts | 8 ++- 6 files changed, 50 insertions(+), 47 deletions(-) diff --git a/feature-libs/user/account/assets/translations/en/userAccount.json b/feature-libs/user/account/assets/translations/en/userAccount.json index 407ea159b5e..aaacc9defa1 100644 --- a/feature-libs/user/account/assets/translations/en/userAccount.json +++ b/feature-libs/user/account/assets/translations/en/userAccount.json @@ -16,7 +16,7 @@ "wrongEmailFormat": "This is not a valid email format." }, "verificationTokenForm": { - "sentOTP": "Verification code has been sent to {{target}}. Please enter the code.", + "createVerificationToken": "Verification code has been sent to {{target}}. Please enter the code.", "sendRateLime": "in {{waitTime}}s", "resend": "Resend", "verificationCode": { @@ -42,4 +42,4 @@ "myAccountV2User": { "signOut": "Sign Out" } -} +} \ No newline at end of file diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html index 6888e267c40..f69cf21c9c2 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html +++ b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.html @@ -5,48 +5,45 @@ >
-
- -
+