From ea1c9d43fb536b19509213054010f299991e0a80 Mon Sep 17 00:00:00 2001 From: Melody-zhou-512 <140146828+Melody-zhou-512@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:53:30 +0800 Subject: [PATCH 01/15] Merge branch 'develop' into feature/CXSPA-3919 --- feature-libs/user/_index.scss | 3 +- .../assets/translations/en/userAccount.json | 2 +- .../account/root/user-account-root.module.ts | 1 + .../assets/translations/en/userProfile.json | 15 +- .../components/otp-login-register/index.ts | 9 + .../otp-login-register.component.html | 246 +++++++++++ .../otp-login-register.component.spec.ts | 413 ++++++++++++++++++ .../otp-login-register.component.ts | 282 ++++++++++++ .../otp-login-register.module.ts | 67 +++ .../otp-register-component.service.spec.ts | 56 +++ .../otp-register-component.service.ts | 49 +++ .../user/profile/components/public_api.ts | 2 + .../register/register.component.html | 1 - .../index.ts | 9 + ...ter-verification-token-form.component.html | 156 +++++++ ...-verification-token-form.component.spec.ts | 234 ++++++++++ ...ister-verification-token-form.component.ts | 273 ++++++++++++ ...register-verification-token-form.module.ts | 63 +++ ...er-verification-token-form.service.spec.ts | 85 ++++ ...egister-verification-token-form.service.ts | 57 +++ .../components/user-account-constants.ts | 7 + .../user-profile-components.module.ts | 4 + .../profile/core/connectors/converters.ts | 15 +- .../core/connectors/user-profile.adapter.ts | 11 +- .../connectors/user-profile.connector.spec.ts | 39 +- .../core/connectors/user-profile.connector.ts | 15 +- .../profile/core/facade/facade-providers.ts | 7 + .../user/profile/core/facade/index.ts | 1 + ...tration-verification-token.service.spec.ts | 68 +++ ...registration-verification-token.service.ts | 45 ++ ...efault-occ-user-profile-endpoint.config.ts | 2 + .../config/occ-user-profile-endpoint.model.ts | 5 + .../adapters/occ-user-profile.adapter.spec.ts | 57 ++- .../occ/adapters/occ-user-profile.adapter.ts | 42 +- .../user/profile/root/facade/index.ts | 1 + .../registration-verification-token.facade.ts | 29 ++ feature-libs/user/profile/root/model/index.ts | 1 + .../root/model/otp-registration.model.ts | 19 + .../profile/root/model/user-profile.model.ts | 2 + .../profile/root/user-profile-root.module.ts | 2 + feature-libs/user/profile/styles/_index.scss | 2 + .../profile/styles/_otp-register-form.scss | 28 ++ .../styles/_verification-token-form.scss | 54 +++ .../assets/src/translations/en/common.json | 1 + .../routing/default-routing-config.ts | 5 + 45 files changed, 2473 insertions(+), 12 deletions(-) create mode 100644 feature-libs/user/profile/components/otp-login-register/index.ts create mode 100644 feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html create mode 100644 feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts create mode 100644 feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts create mode 100644 feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts create mode 100644 feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts create mode 100644 feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts create mode 100644 feature-libs/user/profile/components/registration-verification-token-form/index.ts create mode 100644 feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.html create mode 100644 feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts create mode 100644 feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts create mode 100644 feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts create mode 100644 feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts create mode 100644 feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts create mode 100644 feature-libs/user/profile/components/user-account-constants.ts create mode 100644 feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts create mode 100644 feature-libs/user/profile/core/facade/registration-verification-token.service.ts create mode 100644 feature-libs/user/profile/root/facade/registration-verification-token.facade.ts create mode 100644 feature-libs/user/profile/root/model/otp-registration.model.ts create mode 100644 feature-libs/user/profile/styles/_otp-register-form.scss create mode 100644 feature-libs/user/profile/styles/_verification-token-form.scss diff --git a/feature-libs/user/_index.scss b/feature-libs/user/_index.scss index 280d865eba7..11254250211 100644 --- a/feature-libs/user/_index.scss +++ b/feature-libs/user/_index.scss @@ -6,7 +6,8 @@ $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-login, cx-login-form, cx-register, cx-otp-register-form, + cx-registration-verification-token-form, 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-otp-login-form, cx-verification-token-form, cx-verification-token-dialog !default; diff --git a/feature-libs/user/account/assets/translations/en/userAccount.json b/feature-libs/user/account/assets/translations/en/userAccount.json index 6f7c29744c1..2e214af4b84 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": { - "createVerificationToken": "Verification code sent to {{target}}. Please check and enter the code.", + "createVerificationToken": "Verification code has been sent to {{target}}. Please enter the code.", "needInputCredentials": "Please enter your credentials to continue.", "sendRateLime": "in {{waitTime}} seconds", "resend": "Resend", 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 4dc75ee5a58..559767b869e 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -24,6 +24,7 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { 'ReturningCustomerRegisterComponent', 'MyAccountViewUserComponent', 'ReturningCustomerOTPLoginComponent', + 'ReturningCustomerOTPRegistrationComponent', ], }, // by default core is bundled together with components diff --git a/feature-libs/user/profile/assets/translations/en/userProfile.json b/feature-libs/user/profile/assets/translations/en/userProfile.json index 23e2cc61cd3..401d950b12b 100644 --- a/feature-libs/user/profile/assets/translations/en/userProfile.json +++ b/feature-libs/user/profile/assets/translations/en/userProfile.json @@ -27,11 +27,12 @@ "termsAndConditions": "Terms & Conditions", "signIn": "I already have an account. Sign In", "register": "Register", + "furtherRegistration": "Continue", "confirmNewPassword": "Confirm New Password", "resetPassword": "Reset Password", "createAccount": "Create an account", "title": "Title", - "titleOptional": "Title (optional)", + "titleOptional": "Title", "firstName": { "label": "First name", "placeholder": "First name" @@ -57,7 +58,17 @@ "bothPasswordMustMatch": "Both password must match", "titleRequired": "Title is required.", "postRegisterMessage": "Please log in with provided credentials.", - "postRegisterSuccessMessage": "Successful Registration: Please log in with provided credentials" + "postRegisterSuccessMessage": "Your account has been successfully created! Please log in with provided credentials", + "verificationTokenForm": { + "createVerificationToken": "Verification code has been sent to {{target}}. Please enter the code.", + "sendRateLime": "in {{waitTime}} seconds", + "resend": "Resend", + "verificationCode": { + "label": "Verification Code", + "placeholder": "Enter Verification Code" + }, + "back": "Back" + } }, "forgottenPassword": { "resetPassword": "Reset password", diff --git a/feature-libs/user/profile/components/otp-login-register/index.ts b/feature-libs/user/profile/components/otp-login-register/index.ts new file mode 100644 index 00000000000..b71912923d6 --- /dev/null +++ b/feature-libs/user/profile/components/otp-login-register/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './otp-login-register.component'; +export * from './otp-login-register.module'; +export * from './otp-register-component.service'; diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html new file mode 100644 index 00000000000..080852cc6e0 --- /dev/null +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html @@ -0,0 +1,246 @@ +
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ + + + +
+
+
+
+
+ + + + + + +
+
diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts new file mode 100644 index 00000000000..8e956accacf --- /dev/null +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts @@ -0,0 +1,413 @@ +import { Component, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { AbstractControl, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { + ANONYMOUS_CONSENT_STATUS, + AnonymousConsent, + AnonymousConsentsConfig, + AnonymousConsentsService, + BaseSite, + BaseSiteService, + ClientAuthenticationTokenService, + ConsentTemplate, + GlobalMessageEntities, + GlobalMessageService, + GlobalMessageType, + I18nTestingModule, + LanguageService, + RoutingService, + SiteAdapter, + Title, +} from '@spartacus/core'; +import { + CaptchaModule, + FormErrorsModule, + NgSelectA11yModule, + PasswordVisibilityToggleModule, +} from '@spartacus/storefront'; +import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive'; +import { EMPTY, Observable, of } from 'rxjs'; + +import createSpy = jasmine.createSpy; +import { OneTimePasswordLoginRegisterComponentService } from './otp-register-component.service'; +import { OneTimePasswordLoginRegisterComponent } from './otp-login-register.component'; +import { RegistrationVerificationTokenFacade } from '../../root/facade'; +import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-account-constants'; + +const mockRegisterFormData: any = { + titleCode: 'Mr', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@thebest.john.intheworld.com', + termsandconditions: true, + newsletter: true, + captcha: true, +}; + +const mockTitlesList: Title[] = [ + { + code: 'mr', + name: 'Mr.', + }, + { + code: 'mrs', + name: 'Mrs.', + }, +]; + +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} + +@Component({ + selector: 'cx-spinner', + template: '', +}) +class MockSpinnerComponent {} + +class MockGlobalMessageService { + add = createSpy(); + remove = createSpy(); + get() { + return EMPTY; + } +} + +class MockRoutingService { + go = createSpy(); +} + +class MockAnonymousConsentsService { + getConsent(_templateCode: string): Observable { + return EMPTY; + } + getTemplate(_templateCode: string): Observable { + return EMPTY; + } + withdrawConsent(_templateCode: string): void {} + giveConsent(_templateCode: string): void {} + isConsentGiven(_consent: AnonymousConsent): boolean { + return true; + } +} + +class MockRegistrationVerificationTokenFacade + implements Partial +{ + createRegistrationVerificationToken = createSpy().and.returnValue( + of({ + expiresIn: '300', + tokenId: 'mockTokenId', + }) + ); +} + +const mockAnonymousConsentsConfig: AnonymousConsentsConfig = { + anonymousConsents: { + registerConsent: 'MARKETING', + requiredConsents: ['MARKETING'], + }, +}; + +class MockOneTimePasswordLoginRegisterComponentService + implements Partial +{ + getTitles = createSpy().and.returnValue(of(mockTitlesList)); + getAdditionalConsents = createSpy(); + generateAdditionalConsentsFormControl = createSpy(); +} + +class MockSiteAdapter { + public loadBaseSite(siteUid?: string): Observable { + return of({ + uid: siteUid, + captchaConfig: { + enabled: true, + publicKey: 'mock-key', + }, + }); + } +} + +class MockBaseSiteService { + getActive(): Observable { + return of('mock-site'); + } +} + +class MockLanguageService { + getActive(): Observable { + return of('mock-lang'); + } +} + +class MockClientAuthenticationTokenService + implements Partial +{ + loadClientAuthenticationToken = createSpy().and.returnValue(of(undefined)); +} + +describe('OneTimePasswordLoginRegisterComponent', () => { + let controls: any; + let component: OneTimePasswordLoginRegisterComponent; + let fixture: ComponentFixture; + let mockRoutingService: RoutingService; + + let globalMessageService: GlobalMessageService; + let anonymousConsentService: AnonymousConsentsService; + let registrationVerificationTokenFacade: RegistrationVerificationTokenFacade; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + FormErrorsModule, + NgSelectModule, + PasswordVisibilityToggleModule, + NgSelectA11yModule, + CaptchaModule, + ], + declarations: [ + OneTimePasswordLoginRegisterComponent, + MockUrlPipe, + MockSpinnerComponent, + MockFeatureDirective, + ], + providers: [ + { + provide: OneTimePasswordLoginRegisterComponentService, + useClass: MockOneTimePasswordLoginRegisterComponentService, + }, + { + provide: GlobalMessageService, + useClass: MockGlobalMessageService, + }, + { + provide: RoutingService, + useClass: MockRoutingService, + }, + { + provide: AnonymousConsentsService, + useClass: MockAnonymousConsentsService, + }, + { + provide: AnonymousConsentsConfig, + useValue: mockAnonymousConsentsConfig, + }, + { + provide: SiteAdapter, + useClass: MockSiteAdapter, + }, + { + provide: BaseSiteService, + useClass: MockBaseSiteService, + }, + { + provide: LanguageService, + useClass: MockLanguageService, + }, + { + provide: ClientAuthenticationTokenService, + useClass: MockClientAuthenticationTokenService, + }, + { + provide: RegistrationVerificationTokenFacade, + useClass: MockRegistrationVerificationTokenFacade, + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OneTimePasswordLoginRegisterComponent); + globalMessageService = TestBed.inject(GlobalMessageService); + anonymousConsentService = TestBed.inject(AnonymousConsentsService); + registrationVerificationTokenFacade = TestBed.inject( + RegistrationVerificationTokenFacade + ); + mockRoutingService = TestBed.inject(RoutingService); + + component = fixture.componentInstance; + + fixture.detectChanges(); + controls = component.registerForm.controls; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('submit button', () => { + it('should NOT be disabled', () => { + fixture = TestBed.createComponent(OneTimePasswordLoginRegisterComponent); + fixture.detectChanges(); + const el: HTMLElement = fixture.debugElement.nativeElement; + const submitButton: HTMLElement = el.querySelector( + 'button[type="submit"]' + ); + expect(submitButton.hasAttribute('disabled')).toBeFalsy(); + }); + }); + + describe('ngOnInit', () => { + it('should load titles', () => { + component.ngOnInit(); + + let titleList: Title[]; + component.titles$ + .subscribe((data) => { + titleList = data; + }) + .unsubscribe(); + expect(titleList).toEqual(mockTitlesList); + }); + + it('should handle error when title code is required from the backend config', () => { + spyOn(globalMessageService, 'get').and.returnValue( + of({ + [GlobalMessageType.MSG_TYPE_ERROR]: [ + { raw: 'This field is required.' }, + ], + } as GlobalMessageEntities) + ); + component.ngOnInit(); + + expect(globalMessageService.remove).toHaveBeenCalledWith( + GlobalMessageType.MSG_TYPE_ERROR + ); + expect(globalMessageService.add).toHaveBeenCalledWith( + { + key: 'register.titleRequired', + }, + GlobalMessageType.MSG_TYPE_ERROR + ); + }); + }); + + describe('SendRegistrationVerificationToken', () => { + it('should create registration verification token with valid form', () => { + component.registerForm.patchValue(mockRegisterFormData); + component.ngOnInit(); + component.submitForm(); + expect( + registrationVerificationTokenFacade.createRegistrationVerificationToken + ).toHaveBeenCalledWith({ + loginId: mockRegisterFormData.email.toLowerCase(), + purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE, + }); + }); + + it('should not create registration verification token with valid form', () => { + component.ngOnInit(); + component.submitForm(); + expect( + registrationVerificationTokenFacade.createRegistrationVerificationToken + ).not.toHaveBeenCalled(); + }); + + it('should redirect to next register page', () => { + component.ngOnInit(); + component.SendRegistrationVerificationToken(); + + expect(mockRoutingService.go).toHaveBeenCalled(); + }); + }); + + const toggleAnonymousConsentMethod = 'toggleAnonymousConsent'; + describe(`${toggleAnonymousConsentMethod}`, () => { + it('should call anonymousConsentsService.giveConsent when the consent is given', () => { + spyOn(anonymousConsentService, 'giveConsent').and.stub(); + component.ngOnInit(); + + controls['newsletter'].setValue(true); + component.toggleAnonymousConsent(); + expect(anonymousConsentService.giveConsent).toHaveBeenCalled(); + }); + it('should call anonymousConsentsService.withdrawConsent when the consent is NOT given', () => { + spyOn(anonymousConsentService, 'withdrawConsent').and.stub(); + component.ngOnInit(); + + controls['newsletter'].setValue(false); + component.toggleAnonymousConsent(); + expect(anonymousConsentService.withdrawConsent).toHaveBeenCalled(); + }); + }); + + describe('isConsentGiven', () => { + it('should call anonymousConsentsService.isConsentGiven', () => { + spyOn(anonymousConsentService, 'isConsentGiven').and.stub(); + const mockConsent: AnonymousConsent = { + consentState: ANONYMOUS_CONSENT_STATUS.GIVEN, + }; + component.isConsentGiven(mockConsent); + expect(anonymousConsentService.isConsentGiven).toHaveBeenCalledWith( + mockConsent + ); + }); + }); + + const isConsentRequiredMethod = 'isConsentRequired'; + describe('isConsentRequired', () => { + it('should disable form when register consent is required', () => { + expect(component[isConsentRequiredMethod]()).toEqual(true); + }); + + it('should disable input when register consent is required', () => { + spyOn(component, isConsentRequiredMethod).and.returnValue(true); + fixture.detectChanges(); + expect(controls['newsletter'].status).toEqual('DISABLED'); + }); + }); + + describe('captcha', () => { + let captchaComponent; + beforeEach(() => { + captchaComponent = fixture.debugElement.query(By.css('cx-captcha')); + spyOn(component, 'SendRegistrationVerificationToken').and.callThrough(); + mockRegisterFormData.captcha = false; + component.registerForm.patchValue(mockRegisterFormData); + }); + + function getCaptchaControl( + component: OneTimePasswordLoginRegisterComponent + ): AbstractControl { + return component.registerForm.get('captcha') as AbstractControl; + } + + it('should create captcha component', () => { + expect(captchaComponent).toBeTruthy(); + }); + + it('should enable captcha', () => { + captchaComponent.triggerEventHandler('enabled', true); + component.submitForm(); + + expect(getCaptchaControl(component).valid).toEqual(false); + expect(component.SendRegistrationVerificationToken).toHaveBeenCalledTimes( + 0 + ); + }); + + it('should confirm captcha', () => { + spyOn(component, 'captchaConfirmed').and.callThrough(); + + captchaComponent.triggerEventHandler('enabled', true); + captchaComponent.triggerEventHandler('confirmed', true); + component.submitForm(); + + expect(getCaptchaControl(component).value).toBe(true); + expect(getCaptchaControl(component).valid).toEqual(true); + expect(component.SendRegistrationVerificationToken).toHaveBeenCalledTimes( + 1 + ); + }); + }); +}); diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts new file mode 100644 index 00000000000..233589ed317 --- /dev/null +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts @@ -0,0 +1,282 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { + AnonymousConsent, + AnonymousConsentsConfig, + AnonymousConsentsService, + ClientAuthenticationTokenService, + ConsentTemplate, + GlobalMessageEntities, + GlobalMessageService, + GlobalMessageType, + RoutingService, + useFeatureStyles, +} from '@spartacus/core'; +import { CustomFormValidators, sortTitles } from '@spartacus/storefront'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, + RegistrationVerificationTokenFacade, + Title, +} from '@spartacus/user/profile/root'; +import { + BehaviorSubject, + combineLatest, + filter, + map, + Observable, + Subscription, +} from 'rxjs'; + +import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-account-constants'; +import { OneTimePasswordLoginRegisterComponentService } from './otp-register-component.service'; + +@Component({ + selector: 'cx-otp-register-form', + templateUrl: './otp-login-register.component.html', +}) +export class OneTimePasswordLoginRegisterComponent + implements OnInit, OnDestroy +{ + protected routingService = inject(RoutingService); + protected registrationVerificationTokenFacade = inject( + RegistrationVerificationTokenFacade + ); + + titles$: Observable; + + isLoading$ = new BehaviorSubject(false); + + private subscription = new Subscription(); + + anonymousConsent$: Observable<{ + consent: AnonymousConsent | undefined; + template: string; + }>; + + registerForm: UntypedFormGroup = this.fb.group({ + titleCode: [null], + firstName: ['', Validators.required], + lastName: ['', Validators.required], + email: ['', [Validators.required, CustomFormValidators.emailValidator]], + newsletter: new UntypedFormControl({ + value: false, + disabled: this.isConsentRequired(), + }), + additionalConsents: + this.oneTimePasswordLoginRegisterComponentService.generateAdditionalConsentsFormControl?.() ?? + this.fb.array([]), + termsandconditions: [false, Validators.requiredTrue], + captcha: [false, Validators.requiredTrue], + }); + + additionalRegistrationConsents: { + template: ConsentTemplate; + required: boolean; + }[]; + + get additionalConsents(): UntypedFormArray { + return this.registerForm?.get('additionalConsents') as UntypedFormArray; + } + + updateAdditionalConsents(event: MouseEvent, index: number) { + const { checked } = event.target as HTMLInputElement; + this.registerForm.value.additionalConsents[index] = checked; + } + + constructor( + protected globalMessageService: GlobalMessageService, + protected fb: UntypedFormBuilder, + protected router: RoutingService, + protected anonymousConsentsService: AnonymousConsentsService, + protected anonymousConsentsConfig: AnonymousConsentsConfig, + protected clientAuthenticationTokenService: ClientAuthenticationTokenService, + protected oneTimePasswordLoginRegisterComponentService: OneTimePasswordLoginRegisterComponentService + ) { + useFeatureStyles('a11yPasswordVisibliltyBtnValueOverflow'); + } + + ngOnInit() { + this.titles$ = this.oneTimePasswordLoginRegisterComponentService + .getTitles() + .pipe( + map((titles: Title[]) => { + return titles.sort(sortTitles); + }) + ); + + // TODO: Workaround: allow server for decide is titleCode mandatory (if yes, provide personalized message) + this.subscription.add( + this.globalMessageService + .get() + .pipe(filter((messages) => !!Object.keys(messages).length)) + .subscribe((globalMessageEntities: GlobalMessageEntities) => { + const messages = + globalMessageEntities && + globalMessageEntities[GlobalMessageType.MSG_TYPE_ERROR]; + + if ( + messages && + messages.some( + (message) => message.raw === 'This field is required.' + ) + ) { + this.globalMessageService.remove(GlobalMessageType.MSG_TYPE_ERROR); + this.globalMessageService.add( + { key: 'register.titleRequired' }, + GlobalMessageType.MSG_TYPE_ERROR + ); + } + }) + ); + + const registerConsent = + this.anonymousConsentsConfig?.anonymousConsents?.registerConsent ?? ''; + + this.anonymousConsent$ = combineLatest([ + this.anonymousConsentsService.getConsent(registerConsent), + this.anonymousConsentsService.getTemplate(registerConsent), + ]).pipe( + map( + ([consent, template]: [ + AnonymousConsent | undefined, + ConsentTemplate | undefined, + ]) => { + return { + consent, + template: template?.description ? template.description : '', + }; + } + ) + ); + + this.additionalRegistrationConsents = + this.oneTimePasswordLoginRegisterComponentService?.getAdditionalConsents() || + []; + + this.subscription.add( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.registerForm.get('newsletter')!.valueChanges.subscribe(() => { + this.toggleAnonymousConsent(); + }) + ); + } + + submitForm(): void { + if (this.registerForm.valid) { + this.SendRegistrationVerificationToken(); + } else { + this.registerForm.markAllAsTouched(); + } + } + + SendRegistrationVerificationToken(): void { + this.isLoading$.next(true); + this.clientAuthenticationTokenService.loadClientAuthenticationToken(); + const registrationVerificationTokenCreation = + this.collectDataFromRegisterForm(); + this.registrationVerificationTokenFacade + .createRegistrationVerificationToken( + registrationVerificationTokenCreation + ) + .subscribe({ + next: (result: RegistrationVerificationToken) => + this.goToVerificationTokenForm( + result, + registrationVerificationTokenCreation + ), + error: () => this.isLoading$.next(false), + complete: () => this.onCreateRegistrationVerificationTokenComplete(), + }); + } + + protected onCreateRegistrationVerificationTokenComplete(): void { + this.isLoading$.next(false); + } + + titleSelected(title: Title): void { + this.registerForm['controls'].titleCode.setValue(title.code); + } + + collectDataFromRegisterForm(): RegistrationVerificationTokenCreation { + return { + loginId: this.registerForm.value.email.toLowerCase(), + purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE, + }; + } + + protected goToVerificationTokenForm( + registrationVerificationToken: RegistrationVerificationToken, + registrationVerificationTokenCreation: RegistrationVerificationTokenCreation + ): void { + this.routingService.go( + { + cxRoute: 'verifyTokenForRegistration', + }, + { + state: { + loginId: registrationVerificationTokenCreation.loginId, + tokenId: registrationVerificationToken.tokenId, + expiresIn: registrationVerificationToken.expiresIn, + titleCode: this.registerForm.value.titleCode, + firstName: this.registerForm.value.firstName, + lastName: this.registerForm.value.lastName, + }, + } + ); + } + + isConsentGiven(consent: AnonymousConsent | undefined): boolean { + return this.anonymousConsentsService.isConsentGiven(consent); + } + + private isConsentRequired(): boolean { + const requiredConsents = + this.anonymousConsentsConfig?.anonymousConsents?.requiredConsents; + const registerConsent = + this.anonymousConsentsConfig?.anonymousConsents?.registerConsent; + + if (requiredConsents && registerConsent) { + return requiredConsents.includes(registerConsent); + } + + return false; + } + + toggleAnonymousConsent(): void { + const registerConsent = + this.anonymousConsentsConfig?.anonymousConsents?.registerConsent; + + if (registerConsent) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (Boolean(this.registerForm.get('newsletter')!.value)) { + this.anonymousConsentsService.giveConsent(registerConsent); + } else { + this.anonymousConsentsService.withdrawConsent(registerConsent); + } + } + } + + /** + * Triggered via CaptchaComponent when a user confirms captcha + */ + captchaConfirmed() { + this.registerForm.get('captcha')?.setValue(true); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts new file mode 100644 index 00000000000..e78a4995699 --- /dev/null +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { + CmsConfig, + FeaturesConfigModule, + I18nModule, + NotAuthGuard, + provideDefaultConfig, + UrlModule, +} from '@spartacus/core'; +import { + BtnLikeLinkModule, + CaptchaModule, + FormErrorsModule, + NgSelectA11yModule, + PageSlotModule, + SpinnerModule, +} from '@spartacus/storefront'; +import { OneTimePasswordLoginRegisterComponent } from './otp-login-register.component'; +import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { OneTimePasswordLoginRegisterComponentService } from './otp-register-component.service'; +import { UserRegisterFacade } from '../../root/facade'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + UrlModule, + PageSlotModule, + I18nModule, + FeaturesConfigModule, + BtnLikeLinkModule, + ReactiveFormsModule, + NgSelectModule, + NgSelectA11yModule, + CaptchaModule, + SpinnerModule, + FormErrorsModule, + ], + providers: [ + provideDefaultConfig({ + cmsComponents: { + ReturningCustomerOTPRegisterComponent: { + component: OneTimePasswordLoginRegisterComponent, + guards: [NotAuthGuard], + providers: [ + { + provide: OneTimePasswordLoginRegisterComponentService, + useClass: OneTimePasswordLoginRegisterComponentService, + deps: [UserRegisterFacade, UntypedFormBuilder], + }, + ], + }, + }, + }), + ], + declarations: [OneTimePasswordLoginRegisterComponent], +}) +export class OneTimePasswordLoginRegisterModule {} diff --git a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts new file mode 100644 index 00000000000..a99fd6d6d27 --- /dev/null +++ b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts @@ -0,0 +1,56 @@ +import { inject, TestBed } from '@angular/core/testing'; +import { UntypedFormBuilder } from '@angular/forms'; +import { UserRegisterFacade } from '@spartacus/user/profile/root'; +import { of } from 'rxjs'; + +import createSpy = jasmine.createSpy; +import { OneTimePasswordLoginRegisterComponentService } from './otp-register-component.service'; + +class MockUserRegisterFacade implements Partial { + getTitles = createSpy().and.returnValue(of([])); +} + +describe('OneTimePasswordLoginRegisterComponentService', () => { + let service: OneTimePasswordLoginRegisterComponentService; + let userRegisterFacade: UserRegisterFacade; + let fb: UntypedFormBuilder; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + OneTimePasswordLoginRegisterComponentService, + UntypedFormBuilder, + { provide: UserRegisterFacade, useClass: MockUserRegisterFacade }, + ], + }); + + userRegisterFacade = TestBed.inject(UserRegisterFacade); + fb = TestBed.inject(UntypedFormBuilder); + service = TestBed.inject(OneTimePasswordLoginRegisterComponentService); + }); + + it('should inject OneTimePasswordLoginRegisterComponentService', inject( + [OneTimePasswordLoginRegisterComponentService], + ( + oneTimePasswordLoginRegisterComponentService: OneTimePasswordLoginRegisterComponentService + ) => { + expect(oneTimePasswordLoginRegisterComponentService).toBeTruthy(); + } + )); + + it('should get titles from UserRegisterService', () => { + service.getTitles().subscribe().unsubscribe(); + expect(userRegisterFacade.getTitles).toHaveBeenCalled(); + }); + + it('generateAdditionalConsentsFormControl', () => { + spyOn(fb, 'array').and.callThrough(); + service.generateAdditionalConsentsFormControl(); + expect(fb.array).toHaveBeenCalled(); + }); + + it('getAdditionalConsents', () => { + let result = service.getAdditionalConsents(); + expect(result).toEqual([]); + }); +}); diff --git a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts new file mode 100644 index 00000000000..6c32858588e --- /dev/null +++ b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { UntypedFormArray, UntypedFormBuilder } from '@angular/forms'; +import { ConsentTemplate, Title } from '@spartacus/core'; +import { UserRegisterFacade } from '@spartacus/user/profile/root'; +import { Observable } from 'rxjs'; + +@Injectable() +export class OneTimePasswordLoginRegisterComponentService { + constructor( + protected userRegisterFacade: UserRegisterFacade, + protected fb?: UntypedFormBuilder + ) {} + + /** + * Returns titles that can be used for the user profiles. + */ + getTitles(): Observable { + return this.userRegisterFacade.getTitles(); + } + + /** + * Get if any additional consents needs to be provided during registration + * In core feature, no additional consents are necessary during registration. + * In integration scenarios, eg: cdc, this method will be overridden to return + * necessary cdc consents + */ + getAdditionalConsents(): { + template: ConsentTemplate; + required: boolean; + }[] { + return []; + } + + /** + * Generate form control if any additional consents that needs to be provided during registration + * In core feature, no additional consents are necessary during registration. + * In integration scenarios, eg: cdc, this method will be overridden to return + * form controls for necessary cdc consents + */ + generateAdditionalConsentsFormControl(): UntypedFormArray | undefined { + return this.fb?.array([]) ?? undefined; + } +} diff --git a/feature-libs/user/profile/components/public_api.ts b/feature-libs/user/profile/components/public_api.ts index 4e9ca5c82f1..486cdfd79b6 100644 --- a/feature-libs/user/profile/components/public_api.ts +++ b/feature-libs/user/profile/components/public_api.ts @@ -13,3 +13,5 @@ export * from './update-password/index'; export * from './update-profile/index'; export * from './user-profile-components.module'; export * from './address-book/index'; +export * from './otp-login-register/index'; +export * from './registration-verification-token-form/index'; diff --git a/feature-libs/user/profile/components/register/register.component.html b/feature-libs/user/profile/components/register/register.component.html index e931a943f37..43bba667521 100644 --- a/feature-libs/user/profile/components/register/register.component.html +++ b/feature-libs/user/profile/components/register/register.component.html @@ -10,7 +10,6 @@
-

{{ 'formLegend.required' | cxTranslate }}

diff --git a/feature-libs/user/profile/components/registration-verification-token-form/index.ts b/feature-libs/user/profile/components/registration-verification-token-form/index.ts new file mode 100644 index 00000000000..2df8b8968c7 --- /dev/null +++ b/feature-libs/user/profile/components/registration-verification-token-form/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './verify-register-verification-token-form.module'; +export * from './verify-register-verification-token-form.component'; +export * from './verify-register-verification-token-form.service'; diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.html b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.html new file mode 100644 index 00000000000..add244d8c00 --- /dev/null +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.html @@ -0,0 +1,156 @@ +
+ + + + + + +
+ +
+ +
+ +
+ +
+ + +
+
+ + + + + + +
+
diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts new file mode 100644 index 00000000000..98dec092861 --- /dev/null +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts @@ -0,0 +1,234 @@ +import { ChangeDetectorRef, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { + FeatureConfigService, + I18nTestingModule, + RoutingService, +} from '@spartacus/core'; +import { + FormErrorsModule, + LaunchDialogService, + SpinnerModule, +} from '@spartacus/storefront'; +import { BehaviorSubject, of } from 'rxjs'; +import createSpy = jasmine.createSpy; +import { RegistrationVerificationTokenFormComponentService } from './verify-register-verification-token-form.service'; +import { RegistrationVerificationTokenFormComponent } from './verify-register-verification-token-form.component'; + +const mockRegisterFormData: any = { + titleCode: 'Mr', + firstName: 'John', + lastName: 'Doe', + email: 'JohnDoe@thebest.john.intheworld.com', + tokenId: 'mock_tokenId', + tokenCode: 'mock_tokenCode', + password: 'strongPass$!123', + passwordconf: 'strongPass$!123', +}; + +class MockRoutingService { + go = createSpy(); +} + +class MockFormComponentService + implements Partial +{ + form: UntypedFormGroup = new UntypedFormGroup({ + tokenId: new UntypedFormControl(), + tokenCode: new UntypedFormControl(), + }); + isUpdating$ = new BehaviorSubject(false); + register = createSpy().and.stub(); + createVerificationToken = createSpy().and.returnValue( + of({ tokenId: 'testTokenId', expiresIn: '300' }) + ); + displayMessage = createSpy('displayMessage').and.stub(); +} +@Pipe({ + name: 'cxUrl', +}) +class MockUrlPipe implements PipeTransform { + transform() {} +} + +class MockLaunchDialogService implements Partial { + openDialogAndSubscribe = createSpy().and.stub(); +} + +class MockRegistrationVerificationTokenFormComponentService + implements Partial +{ + register = createSpy().and.returnValue(of(mockRegisterFormData)); + postRegisterMessage = createSpy(); + displayMessage = createSpy(); +} + +describe('RegistrationVerificationTokenFormComponent', () => { + let component: RegistrationVerificationTokenFormComponent; + let fixture: ComponentFixture; + let service: RegistrationVerificationTokenFormComponentService; + let launchDialogService: LaunchDialogService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + RouterTestingModule, + I18nTestingModule, + FormErrorsModule, + SpinnerModule, + ], + declarations: [RegistrationVerificationTokenFormComponent, MockUrlPipe], + providers: [ + { + provide: RegistrationVerificationTokenFormComponentService, + useClass: MockFormComponentService, + }, + { + provide: LaunchDialogService, + useClass: MockLaunchDialogService, + }, + { + provide: RoutingService, + useClass: MockRoutingService, + }, + { + provide: RegistrationVerificationTokenFormComponentService, + useClass: MockRegistrationVerificationTokenFormComponentService, + }, + ChangeDetectorRef, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent( + RegistrationVerificationTokenFormComponent + ); + service = TestBed.inject(RegistrationVerificationTokenFormComponentService); + launchDialogService = TestBed.inject(LaunchDialogService); + component = fixture.componentInstance; + fixture.detectChanges(); + history.pushState( + { + tokenId: 'mock_tokenId', + loginId: 'JohnDoe@thebest.john.intheworld.com', + titleCode: 'Mr', + firstName: 'John', + lastName: 'Doe', + }, + '' + ); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + describe('register', () => { + it('should register with valid form', () => { + component.registerForm.patchValue(mockRegisterFormData); + component.ngOnInit(); + component.onSubmit(); + expect(service.register).toHaveBeenCalledWith({ + firstName: mockRegisterFormData.firstName, + lastName: mockRegisterFormData.lastName, + uid: mockRegisterFormData.email.toLowerCase(), + password: mockRegisterFormData.password, + titleCode: mockRegisterFormData.titleCode, + verificationTokenId: mockRegisterFormData.tokenId, + verificationTokenCode: mockRegisterFormData.tokenCode, + }); + }); + + it('should not register with valid form', () => { + component.ngOnInit(); + component.onSubmit(); + expect(service.register).not.toHaveBeenCalled(); + }); + + it('should display info dialog', () => { + component.openInfoDailog(); + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + }); + + it('should resend OTP', () => { + component.target = 'example@example.com'; + spyOn(component, 'startWaitTimeInterval'); + spyOn(component, 'createRegistrationVerificationToken').and.returnValue( + of({ tokenId: 'mock_tokenId', expiresIn: '300' }) + ); + + component.resendOTP(); + + expect(component.isResendDisabled).toBe(true); + expect(component.waitTime).toBe(60); + expect(component.startWaitTimeInterval).toHaveBeenCalled(); + expect(component.createRegistrationVerificationToken).toHaveBeenCalled(); + expect(service.displayMessage).toHaveBeenCalledWith( + 'example@example.com' + ); + }); + }); + + describe('password validators', () => { + let featureConfigService: FeatureConfigService; + + it('should have new validators when feature flag is enabled', () => { + featureConfigService = TestBed.inject(FeatureConfigService); + spyOn(featureConfigService, 'isEnabled').and.returnValue(true); + + fixture = TestBed.createComponent( + RegistrationVerificationTokenFormComponent + ); + component = fixture.componentInstance; + + fixture.detectChanges(); + + const passwordControl = component.registerForm.get( + 'password' + ) as UntypedFormControl; + const validators = passwordControl.validator + ? passwordControl.validator({} as any) + : []; + + expect(passwordControl).toBeTruthy(); + expect(validators).toEqual({ + required: true, + cxMinOneDigit: true, + cxMinOneSpecialCharacter: true, + cxMinOneUpperCaseCharacter: true, + cxMinEightCharactersLength: true, + cxMaxCharactersLength: true, + }); + }); + + it('should have old validators when feature flag is not enabled', () => { + featureConfigService = TestBed.inject(FeatureConfigService); + spyOn(featureConfigService, 'isEnabled').and.returnValue(false); + + fixture = TestBed.createComponent( + RegistrationVerificationTokenFormComponent + ); + component = fixture.componentInstance; + + fixture.detectChanges(); + + const passwordControl = component.registerForm.get( + 'password' + ) as UntypedFormControl; + const validators = passwordControl.validator + ? passwordControl.validator({} as any) + : []; + + expect(passwordControl).toBeTruthy(); + expect(validators).toEqual({ required: true, cxInvalidPassword: true }); + }); + }); +}); diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts new file mode 100644 index 00000000000..4e9513be952 --- /dev/null +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts @@ -0,0 +1,273 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectorRef, + Component, + ElementRef, + OnInit, + ViewChild, + inject, +} from '@angular/core'; +import { + UntypedFormBuilder, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { + CustomFormValidators, + LAUNCH_CALLER, + LaunchDialogService, +} from '@spartacus/storefront'; +import { BehaviorSubject } from 'rxjs'; +import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-account-constants'; +import { RegistrationVerificationTokenFormComponentService } from './verify-register-verification-token-form.service'; +import { + AuthConfigService, + FeatureConfigService, + OAuthFlow, + RoutingService, +} from '@spartacus/core'; +import { RegistrationVerificationToken, UserSignUp } from '../../root/model'; +import { RegistrationVerificationTokenFacade } from '../../root/facade'; +import { HttpErrorResponse } from '@angular/common/http'; + +@Component({ + selector: 'cx-registration-verification-token-form', + templateUrl: './verify-register-verification-token-form.component.html', +}) +export class RegistrationVerificationTokenFormComponent implements OnInit { + constructor( + protected fb: UntypedFormBuilder, + protected router: RoutingService, + protected authConfigService: AuthConfigService, + protected registrationVerificationTokenFacade: RegistrationVerificationTokenFacade + ) {} + protected service: RegistrationVerificationTokenFormComponentService = inject( + RegistrationVerificationTokenFormComponentService + ); + + protected launchDialogService: LaunchDialogService = + inject(LaunchDialogService); + + private featureConfigService = inject(FeatureConfigService); + protected passwordValidators = this.featureConfigService?.isEnabled( + 'formErrorsDescriptiveMessages' + ) + ? this.featureConfigService.isEnabled('enableSecurePasswordValidation') + ? CustomFormValidators.securePasswordValidators + : this.featureConfigService.isEnabled( + 'enableConsecutiveCharactersPasswordRequirement' + ) + ? [ + ...CustomFormValidators.passwordValidators, + CustomFormValidators.noConsecutiveCharacters, + ] + : CustomFormValidators.passwordValidators + : [ + this.featureConfigService.isEnabled('enableSecurePasswordValidation') + ? CustomFormValidators.securePasswordValidator + : this.featureConfigService.isEnabled( + 'enableConsecutiveCharactersPasswordRequirement' + ) + ? CustomFormValidators.strongPasswordValidator + : CustomFormValidators.passwordValidator, + ]; + + protected cdr: ChangeDetectorRef = inject(ChangeDetectorRef); + + waitTime: number = 60; + + registerForm: UntypedFormGroup = this.fb.group( + { + password: ['', [Validators.required, ...this.passwordValidators]], + passwordconf: ['', Validators.required], + tokenCode: ['', Validators.required], + titleCode: [''], + firstName: [''], + lastName: [''], + email: [''], + tokenId: [''], + }, + { + validators: CustomFormValidators.passwordsMustMatch( + 'password', + 'passwordconf' + ), + } + ); + + ngOnInit() { + if (!!history.state) { + this.tokenId = history.state['tokenId']; + this.target = history.state['loginId']; + this.titleCode = history.state['titleCode']; + this.firstName = history.state['firstName']; + this.lastName = history.state['lastName']; + + history.pushState( + { + tokenId: '', + loginId: '', + titleCode: '', + firstName: '', + lastName: '', + }, + 'verifyTokenForRegistration' + ); + } + + if (!this.target || !this.tokenId || !this.firstName || !this.lastName) { + this.router.go(['/login/register']); + } else { + this.startWaitTimeInterval(); + this.service.displayMessage(this.target); + } + } + + isLoading$ = new BehaviorSubject(false); + + @ViewChild('noReceiveCodeLink') element: ElementRef; + + @ViewChild('resendLink') resendLink: ElementRef; + + tokenId: string; + + tokenCode: string; + + target: string; + + titleCode: string; + + firstName: string; + + lastName: string; + + password: string; + + passwordconf: string; + + isResendDisabled: boolean = true; + + onSubmit(): void { + if (this.registerForm.valid) { + this.registerUserWithVerificationToken(); + } else { + this.registerForm.markAllAsTouched(); + } + } + + registerUserWithVerificationToken(): void { + this.isLoading$.next(true); + this.registerForm.setValue({ + titleCode: this.titleCode, + firstName: this.firstName, + lastName: this.lastName, + email: this.target, + tokenId: this.tokenId, + password: this.registerForm.value.password, + tokenCode: this.registerForm.value.tokenCode, + passwordconf: this.registerForm.value.passwordconf, + }); + this.service + .register(this.collectDataFromRegisterForm(this.registerForm.value)) + .subscribe({ + next: () => this.onRegisterUserSuccess(), + complete: () => this.isLoading$.next(false), + error: (error: HttpErrorResponse) => { + if (error.status == 400) { + this.registerForm + .get('tokenCode') + ?.setErrors({ invalidTokenCodeError: error.message }); + } + this.isLoading$.next(false); + }, + }); + } + + collectDataFromRegisterForm(formData: any): UserSignUp { + const { + firstName, + lastName, + email, + password, + titleCode, + tokenId: verificationTokenId, + tokenCode: verificationTokenCode, + } = formData; + + return { + firstName, + lastName, + uid: email.toLowerCase(), + password, + titleCode, + verificationTokenId, + verificationTokenCode, + }; + } + + protected onRegisterUserSuccess(): void { + if ( + this.authConfigService.getOAuthFlow() === + OAuthFlow.ResourceOwnerPasswordFlow + ) { + this.router.go('login'); + } + this.service.postRegisterMessage(); + } + + resendOTP(): void { + this.isResendDisabled = true; + this.resendLink.nativeElement.tabIndex = -1; + this.waitTime = 60; + this.startWaitTimeInterval(); + this.createRegistrationVerificationToken( + this.target, + ONE_TIME_PASSWORD_REGISTRATION_PURPOSE + ).subscribe({ + next: (result: RegistrationVerificationToken) => + (this.tokenId = result.tokenId), + complete: () => this.service.displayMessage(this.target), + }); + } + + createRegistrationVerificationToken(loginId: string, purpose: string) { + return this.registrationVerificationTokenFacade.createRegistrationVerificationToken( + { + loginId, + purpose, + } + ); + } + + startWaitTimeInterval(): void { + const interval = setInterval(() => { + this.waitTime--; + this.cdr.detectChanges(); + if (this.waitTime <= 0) { + clearInterval(interval); + this.isResendDisabled = false; + this.resendLink.nativeElement.tabIndex = 0; + this.cdr.detectChanges(); + } + }, 1000); + } + + openInfoDailog(): void { + this.launchDialogService.openDialogAndSubscribe( + LAUNCH_CALLER.ACCOUNT_VERIFICATION_TOKEN, + this.element + ); + } + + onOpenInfoDailogKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.openInfoDailog(); + } + } +} diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts new file mode 100644 index 00000000000..ff341c818da --- /dev/null +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts @@ -0,0 +1,63 @@ +/* + * 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, + GlobalMessageService, + I18nModule, + NotAuthGuard, + UrlModule, + provideDefaultConfig, +} from '@spartacus/core'; +import { + FormErrorsModule, + IconModule, + KeyboardFocusModule, + SpinnerModule, +} from '@spartacus/storefront'; +import { RegistrationVerificationTokenFormComponent } from './verify-register-verification-token-form.component'; +import { RegistrationVerificationTokenFormComponentService } from './verify-register-verification-token-form.service'; +import { UserRegisterFacade } from '../../root/facade'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + KeyboardFocusModule, + ReactiveFormsModule, + RouterModule, + UrlModule, + IconModule, + I18nModule, + FormErrorsModule, + SpinnerModule, + FeaturesConfigModule, + ], + providers: [ + provideDefaultConfig({ + cmsComponents: { + VerifyOTPTokenForRegistrationComponent: { + component: RegistrationVerificationTokenFormComponent, + guards: [NotAuthGuard], + providers: [ + { + provide: RegistrationVerificationTokenFormComponentService, + useClass: RegistrationVerificationTokenFormComponentService, + deps: [GlobalMessageService, UserRegisterFacade], + }, + ], + }, + }, + }), + ], + declarations: [RegistrationVerificationTokenFormComponent], +}) +export class RegistrationVerificationTokenFormModule {} diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts new file mode 100644 index 00000000000..870f833813b --- /dev/null +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts @@ -0,0 +1,85 @@ +import { inject, TestBed } from '@angular/core/testing'; +import { UntypedFormBuilder } from '@angular/forms'; +import { + FeatureConfigService, + GlobalMessageService, + GlobalMessageType, +} from '@spartacus/core'; +import { UserRegisterFacade, UserSignUp } from '@spartacus/user/profile/root'; +import { of } from 'rxjs'; + +import createSpy = jasmine.createSpy; +import { RegistrationVerificationTokenFormComponentService } from './verify-register-verification-token-form.service'; + +class MockUserRegisterFacade implements Partial { + getTitles = createSpy().and.returnValue(of([])); + register = createSpy().and.callFake((user: any) => of(user)); +} +class MockGlobalMessageService implements Partial { + add = createSpy(); +} + +class MockFeatureConfigService { + isEnabled() { + return true; + } +} + +describe('RegistrationVerificationTokenFormComponentService', () => { + let service: RegistrationVerificationTokenFormComponentService; + let userRegisterFacade: UserRegisterFacade; + let globalMessageService: GlobalMessageService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + RegistrationVerificationTokenFormComponentService, + UntypedFormBuilder, + { provide: UserRegisterFacade, useClass: MockUserRegisterFacade }, + { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + { provide: FeatureConfigService, useClass: MockFeatureConfigService }, + ], + }); + + globalMessageService = TestBed.inject(GlobalMessageService); + userRegisterFacade = TestBed.inject(UserRegisterFacade); + service = TestBed.inject(RegistrationVerificationTokenFormComponentService); + }); + + it('should inject RegistrationVerificationTokenFormComponentService', inject( + [RegistrationVerificationTokenFormComponentService], + ( + registrationVerificationTokenFormComponentService: RegistrationVerificationTokenFormComponentService + ) => { + expect(registrationVerificationTokenFormComponentService).toBeTruthy(); + } + )); + + it('should be able to register user from UserRegisterService', () => { + const userRegisterFormData: UserSignUp = { + titleCode: 'Mr.', + firstName: 'firstName', + lastName: 'lastName', + uid: 'uid', + password: 'password', + }; + service.register(userRegisterFormData); + expect(userRegisterFacade.register).toHaveBeenCalledWith({ + titleCode: 'Mr.', + firstName: 'firstName', + lastName: 'lastName', + uid: 'uid', + password: 'password', + }); + }); + + describe('postRegisterMessage', () => { + it('should delegate to globalMessageService.add', () => { + service.postRegisterMessage(); + expect(globalMessageService.add).toHaveBeenCalledWith( + { key: 'register.postRegisterSuccessMessage' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + }); + }); +}); diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts new file mode 100644 index 00000000000..6b000e0d213 --- /dev/null +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, inject } from '@angular/core'; +import { + FeatureConfigService, + GlobalMessageService, + GlobalMessageType, + User, +} from '@spartacus/core'; +import { UserRegisterFacade } from '../../root/facade'; +import { UserSignUp } from '../../root/model'; +import { Observable } from 'rxjs'; + +const globalMsgShowTime: number = 10000; +@Injectable() +export class RegistrationVerificationTokenFormComponentService { + constructor( + protected globalMessageService: GlobalMessageService, + protected userRegisterFacade: UserRegisterFacade + ) {} + + private featureConfigService: FeatureConfigService = + inject(FeatureConfigService); + + displayMessage(target: string) { + this.globalMessageService.add( + { + key: 'verificationTokenForm.createVerificationToken', + params: { target }, + }, + GlobalMessageType.MSG_TYPE_CONFIRMATION, + globalMsgShowTime + ); + } + + register(user: UserSignUp): Observable { + return this.userRegisterFacade.register(user); + } + + postRegisterMessage(): void { + if (this.featureConfigService.isEnabled('a11yPostRegisterSuccessMessage')) { + this.globalMessageService.add( + { key: 'register.postRegisterSuccessMessage' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + } else { + this.globalMessageService.add( + { key: 'register.postRegisterMessage' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + } + } +} diff --git a/feature-libs/user/profile/components/user-account-constants.ts b/feature-libs/user/profile/components/user-account-constants.ts new file mode 100644 index 00000000000..c690e9cf266 --- /dev/null +++ b/feature-libs/user/profile/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_REGISTRATION_PURPOSE = 'REGISTRATION'; diff --git a/feature-libs/user/profile/components/user-profile-components.module.ts b/feature-libs/user/profile/components/user-profile-components.module.ts index 86a74b75816..25d6c5ae553 100644 --- a/feature-libs/user/profile/components/user-profile-components.module.ts +++ b/feature-libs/user/profile/components/user-profile-components.module.ts @@ -13,6 +13,8 @@ import { UpdateEmailModule } from './update-email/update-email.module'; import { UpdatePasswordModule } from './update-password/update-password.module'; import { UpdateProfileModule } from './update-profile/update-profile.module'; import { AddressBookModule } from './address-book'; +import { OneTimePasswordLoginRegisterModule } from './otp-login-register'; +import { RegistrationVerificationTokenFormModule } from './registration-verification-token-form'; @NgModule({ imports: [ @@ -24,6 +26,8 @@ import { AddressBookModule } from './address-book'; ResetPasswordModule, CloseAccountModule, AddressBookModule, + OneTimePasswordLoginRegisterModule, + RegistrationVerificationTokenFormModule, ], }) export class UserProfileComponentsModule {} diff --git a/feature-libs/user/profile/core/connectors/converters.ts b/feature-libs/user/profile/core/connectors/converters.ts index 1360c111ae4..1fff21d68a9 100644 --- a/feature-libs/user/profile/core/connectors/converters.ts +++ b/feature-libs/user/profile/core/connectors/converters.ts @@ -7,7 +7,12 @@ import { InjectionToken } from '@angular/core'; import { Converter } from '@spartacus/core'; import { User } from '@spartacus/user/account/root'; -import { Title, UserSignUp } from '@spartacus/user/profile/root'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, + Title, + UserSignUp, +} from '@spartacus/user/profile/root'; export const USER_PROFILE_NORMALIZER = new InjectionToken>( 'UserProfileNormalizer' @@ -28,3 +33,11 @@ export const USER_SIGN_UP_SERIALIZER = new InjectionToken< export const TITLE_NORMALIZER = new InjectionToken>( 'TitleNormalizer' ); + +export const REGISTRATION_FORM_SERIALIZER = new InjectionToken< + Converter +>('RegistrationFormSerializer'); + +export const VERIFICATION_TOKEN_NORMALIZER = new InjectionToken< + Converter +>('VerificationTokenNormalizer'); diff --git a/feature-libs/user/profile/core/connectors/user-profile.adapter.ts b/feature-libs/user/profile/core/connectors/user-profile.adapter.ts index 5735da8f2b0..9719ffed97f 100644 --- a/feature-libs/user/profile/core/connectors/user-profile.adapter.ts +++ b/feature-libs/user/profile/core/connectors/user-profile.adapter.ts @@ -6,7 +6,12 @@ import { User } from '@spartacus/user/account/root'; import { Observable } from 'rxjs'; -import { Title, UserSignUp } from '@spartacus/user/profile/root'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, + Title, + UserSignUp, +} from '@spartacus/user/profile/root'; export abstract class UserProfileAdapter { abstract update(username: string, user: User): Observable; @@ -39,4 +44,8 @@ export abstract class UserProfileAdapter { abstract close(userId: string): Observable; abstract loadTitles(): Observable; + + abstract createRegistrationVerificationToken( + registrationVerificationTokenCreation: RegistrationVerificationTokenCreation + ): Observable; } diff --git a/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts b/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts index 39b43375b7f..1d522b5753a 100644 --- a/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts +++ b/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts @@ -1,5 +1,9 @@ import { TestBed } from '@angular/core/testing'; -import { UserSignUp } from '@spartacus/user/profile/root'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, + UserSignUp, +} from '@spartacus/user/profile/root'; import { of } from 'rxjs'; import { UserProfileAdapter } from './user-profile.adapter'; import { UserProfileConnector } from './user-profile.connector'; @@ -19,6 +23,14 @@ class MockUserAdapter implements UserProfileAdapter { updateEmail = createSpy('updateEmail').and.returnValue(of({})); updatePassword = createSpy('updatePassword').and.returnValue(of({})); loadTitles = createSpy('loadTitles').and.returnValue(of([])); + createRegistrationVerificationToken = createSpy( + 'createRegistrationVerificationToken' + ).and.callFake(() => { + return of({ + expiresIn: '300', + tokenId: 'mockTokenId', + }); + }); } describe('UserConnector', () => { @@ -131,4 +143,29 @@ describe('UserConnector', () => { expect(result).toEqual([]); expect(adapter.loadTitles).toHaveBeenCalledWith(); }); + + it('createRegistrationVerificationToken should call adapter', () => { + const regsitrationVerificationTokenCreation: RegistrationVerificationTokenCreation = + { + purpose: 'REGISTRATION', + loginId: 'test@email.com', + }; + + const registrationVerificationToken: RegistrationVerificationToken = { + expiresIn: '300', + tokenId: 'mockTokenId', + }; + + let result; + + service + .createRegistrationVerificationToken( + regsitrationVerificationTokenCreation + ) + .subscribe((res) => (result = res)); + expect(result).toEqual(registrationVerificationToken); + expect(adapter.createRegistrationVerificationToken).toHaveBeenCalledWith( + regsitrationVerificationTokenCreation + ); + }); }); diff --git a/feature-libs/user/profile/core/connectors/user-profile.connector.ts b/feature-libs/user/profile/core/connectors/user-profile.connector.ts index 9ba1be80b4c..f9fd76dc5eb 100644 --- a/feature-libs/user/profile/core/connectors/user-profile.connector.ts +++ b/feature-libs/user/profile/core/connectors/user-profile.connector.ts @@ -7,7 +7,12 @@ import { Injectable } from '@angular/core'; import { User } from '@spartacus/user/account/root'; import { Observable } from 'rxjs'; -import { Title, UserSignUp } from '@spartacus/user/profile/root'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, + Title, + UserSignUp, +} from '@spartacus/user/profile/root'; import { UserProfileAdapter } from './user-profile.adapter'; @Injectable() @@ -65,4 +70,12 @@ export class UserProfileConnector { getTitles(): Observable { return this.userProfileAdapter.loadTitles(); } + + createRegistrationVerificationToken( + registrationVerificationTokenCreation: RegistrationVerificationTokenCreation + ): Observable { + return this.userProfileAdapter.createRegistrationVerificationToken( + registrationVerificationTokenCreation + ); + } } diff --git a/feature-libs/user/profile/core/facade/facade-providers.ts b/feature-libs/user/profile/core/facade/facade-providers.ts index 80ead5520b6..44828a9acf4 100644 --- a/feature-libs/user/profile/core/facade/facade-providers.ts +++ b/feature-libs/user/profile/core/facade/facade-providers.ts @@ -15,6 +15,8 @@ import { UserRegisterFacade, } from '@spartacus/user/profile/root'; import { Provider } from '@angular/core'; +import { RegistrationVerificationTokenService } from './registration-verification-token.service'; +import { RegistrationVerificationTokenFacade } from 'feature-libs/user/profile/root/facade/registration-verification-token.facade'; export const facadeProviders: Provider[] = [ UserEmailService, UserPasswordService, @@ -36,4 +38,9 @@ export const facadeProviders: Provider[] = [ provide: UserRegisterFacade, useExisting: UserRegisterService, }, + RegistrationVerificationTokenService, + { + provide: RegistrationVerificationTokenFacade, + useExisting: RegistrationVerificationTokenService, + }, ]; diff --git a/feature-libs/user/profile/core/facade/index.ts b/feature-libs/user/profile/core/facade/index.ts index 943a540191c..7b31551afc1 100644 --- a/feature-libs/user/profile/core/facade/index.ts +++ b/feature-libs/user/profile/core/facade/index.ts @@ -8,3 +8,4 @@ export * from './user-email.service'; export * from './user-password.service'; export * from './user-profile.service'; export * from './user-register.service'; +export * from './registration-verification-token.service'; diff --git a/feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts b/feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts new file mode 100644 index 00000000000..fd21b7b2b52 --- /dev/null +++ b/feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts @@ -0,0 +1,68 @@ +import { TestBed } from '@angular/core/testing'; +import { CommandService } from '@spartacus/core'; + +import { of } from 'rxjs'; +import createSpy = jasmine.createSpy; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, +} from '../../root/model'; +import { UserProfileConnector } from '../connectors'; +import { RegistrationVerificationTokenService } from './registration-verification-token.service'; + +const registrationVerificationTokenCreation: RegistrationVerificationTokenCreation = + { + purpose: 'REGISTRATION', + loginId: 'test@email.com', + }; + +const registrationVerificationToken: RegistrationVerificationToken = { + expiresIn: '300', + tokenId: 'mockTokenId', +}; + +class MockUserProfileConnector implements Partial { + createRegistrationVerificationToken = createSpy().and.callFake(() => + of(registrationVerificationToken) + ); +} + +describe('RegistrationVerificationTokenService', () => { + let service: RegistrationVerificationTokenService; + let connector: UserProfileConnector; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: UserProfileConnector, useClass: MockUserProfileConnector }, + CommandService, + RegistrationVerificationTokenService, + ], + }); + + service = TestBed.inject(RegistrationVerificationTokenService); + connector = TestBed.inject(UserProfileConnector); + }); + + it('should inject RegistrationVerificationTokenService', () => { + expect(service).toBeTruthy(); + }); + + describe('create registration verification token', () => { + it('should create registration verification token for given email', () => { + let result: RegistrationVerificationToken | undefined; + service + .createRegistrationVerificationToken( + registrationVerificationTokenCreation + ) + .subscribe((data) => { + result = data; + }) + .unsubscribe(); + expect(result).toEqual(registrationVerificationToken); + expect( + connector.createRegistrationVerificationToken + ).toHaveBeenCalledWith(registrationVerificationTokenCreation); + }); + }); +}); diff --git a/feature-libs/user/profile/core/facade/registration-verification-token.service.ts b/feature-libs/user/profile/core/facade/registration-verification-token.service.ts new file mode 100644 index 00000000000..f535318d337 --- /dev/null +++ b/feature-libs/user/profile/core/facade/registration-verification-token.service.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, inject } from '@angular/core'; +import { Command, CommandService } from '@spartacus/core'; +import { Observable } from 'rxjs'; +import { UserProfileConnector } from '../connectors'; +import { RegistrationVerificationTokenFacade } from 'feature-libs/user/profile/root/facade/registration-verification-token.facade'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, +} from '../../root/model'; + +@Injectable() +export class RegistrationVerificationTokenService + implements RegistrationVerificationTokenFacade +{ + protected connector = inject(UserProfileConnector); + protected command = inject(CommandService); + + protected createRegistrationVerificationTokenCommand: Command< + { + registrationVerificationTokenCreation: RegistrationVerificationTokenCreation; + }, + RegistrationVerificationToken + > = this.command.create(({ registrationVerificationTokenCreation }) => + this.connector.createRegistrationVerificationToken( + registrationVerificationTokenCreation + ) + ); + + /** + * create verification token for registration + */ + createRegistrationVerificationToken( + registrationVerificationTokenCreation: RegistrationVerificationTokenCreation + ): Observable { + return this.createRegistrationVerificationTokenCommand.execute({ + registrationVerificationTokenCreation, + }); + } +} diff --git a/feature-libs/user/profile/occ/adapters/config/default-occ-user-profile-endpoint.config.ts b/feature-libs/user/profile/occ/adapters/config/default-occ-user-profile-endpoint.config.ts index 2c2f03c5c6b..f1d412c16e8 100644 --- a/feature-libs/user/profile/occ/adapters/config/default-occ-user-profile-endpoint.config.ts +++ b/feature-libs/user/profile/occ/adapters/config/default-occ-user-profile-endpoint.config.ts @@ -17,6 +17,8 @@ export const defaultOccUserProfileConfig: OccConfig = { userUpdateLoginId: 'users/${userId}/login', userUpdatePassword: 'users/${userId}/password', titles: 'titles', + createRegistrationVerificationToken: + 'users/anonymous/verificationToken', }, }, }, diff --git a/feature-libs/user/profile/occ/adapters/config/occ-user-profile-endpoint.model.ts b/feature-libs/user/profile/occ/adapters/config/occ-user-profile-endpoint.model.ts index 2719368555b..a234f4add95 100644 --- a/feature-libs/user/profile/occ/adapters/config/occ-user-profile-endpoint.model.ts +++ b/feature-libs/user/profile/occ/adapters/config/occ-user-profile-endpoint.model.ts @@ -51,6 +51,11 @@ export interface UserProfileOccEndpoints { * Close user account */ userCloseAccount?: string | OccEndpoint; + + /** + * Create one time password for user registration + */ + createRegistrationVerificationToken?: string | OccEndpoint; } declare module '@spartacus/core' { interface OccEndpoints extends UserProfileOccEndpoints {} diff --git a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts index ccacf256763..d102ddbd77e 100644 --- a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts +++ b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts @@ -18,8 +18,13 @@ import { TITLE_NORMALIZER, USER_PROFILE_SERIALIZER, USER_SIGN_UP_SERIALIZER, + VERIFICATION_TOKEN_NORMALIZER, } from '@spartacus/user/profile/core'; -import { UserSignUp } from '@spartacus/user/profile/root'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, + UserSignUp, +} from '@spartacus/user/profile/root'; import { OccUserProfileAdapter } from './occ-user-profile.adapter'; import { Observable, of } from 'rxjs'; import { CaptchaApiConfig, CaptchaRenderer } from '@spartacus/storefront'; @@ -91,6 +96,17 @@ const mockCaptchaApiConfig: CaptchaApiConfig = { captchaRenderer: MockCaptchaService, }; +const registrationVerificationTokenCreation: RegistrationVerificationTokenCreation = + { + purpose: 'REGISTRATION', + loginId: 'test@email.com', + }; + +const registrationVerificationToken: RegistrationVerificationToken = { + expiresIn: '300', + tokenId: 'mockTokenId', +}; + describe('OccUserProfileAdapter', () => { let occUserAdapter: OccUserProfileAdapter; let httpMock: HttpTestingController; @@ -384,4 +400,43 @@ describe('OccUserProfileAdapter', () => { expect(converter.pipeableMany).toHaveBeenCalledWith(TITLE_NORMALIZER); }); }); + + describe('create registration verification token', () => { + it('should create registration verification token for given email', () => { + occUserAdapter + .createRegistrationVerificationToken( + registrationVerificationTokenCreation + ) + .subscribe((result) => { + expect(result).toEqual(registrationVerificationToken); + }); + + const mockReq = httpMock.expectOne((req) => { + return req.method === 'POST'; + }); + + expect(occEndpointsService.buildUrl).toHaveBeenCalledWith( + 'createRegistrationVerificationToken' + ); + expect(mockReq.cancelled).toBeFalsy(); + expect(mockReq.request.responseType).toEqual('json'); + mockReq.flush(registrationVerificationToken); + }); + + it('should use converter', () => { + occUserAdapter + .createRegistrationVerificationToken( + registrationVerificationTokenCreation + ) + .subscribe(); + httpMock + .expectOne((req) => { + return req.method === 'POST'; + }) + .flush(registrationVerificationToken); + expect(converter.pipeable).toHaveBeenCalledWith( + VERIFICATION_TOKEN_NORMALIZER + ); + }); + }); }); diff --git a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.ts b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.ts index cf25dd5d9f9..638e3d00458 100644 --- a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.ts +++ b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.ts @@ -18,13 +18,20 @@ import { } from '@spartacus/core'; import { User } from '@spartacus/user/account/root'; import { + REGISTRATION_FORM_SERIALIZER, TITLE_NORMALIZER, USER_PROFILE_NORMALIZER, USER_PROFILE_SERIALIZER, USER_SIGN_UP_SERIALIZER, UserProfileAdapter, + VERIFICATION_TOKEN_NORMALIZER, } from '@spartacus/user/profile/core'; -import { Title, UserSignUp } from '@spartacus/user/profile/root'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, + Title, + UserSignUp, +} from '@spartacus/user/profile/root'; import { Observable } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { CaptchaApiConfig, CaptchaRenderer } from '@spartacus/storefront'; @@ -45,7 +52,6 @@ export class OccUserProfileAdapter implements UserProfileAdapter { protected occEndpoints: OccEndpointsService, protected converter: ConverterService ) {} - update(userId: string, user: User): Observable { const endpoint = this.occEndpoints.isConfigured('userUpdateProfile') ? 'userUpdateProfile' @@ -201,4 +207,36 @@ export class OccUserProfileAdapter implements UserProfileAdapter { } return currentHeaders; } + createRegistrationVerificationToken( + registrationVerificationTokenCreation: RegistrationVerificationTokenCreation + ): Observable { + const url = this.occEndpoints.buildUrl( + 'createRegistrationVerificationToken' + ); + + const headers = InterceptorUtil.createHeader( + USE_CLIENT_TOKEN, + true, + new HttpHeaders({ + ...CONTENT_TYPE_JSON_HEADER, + }) + ); + registrationVerificationTokenCreation = this.converter.convert( + registrationVerificationTokenCreation, + REGISTRATION_FORM_SERIALIZER + ); + + return this.http + .post( + url, + registrationVerificationTokenCreation, + { headers } + ) + .pipe( + catchError((error) => { + throw normalizeHttpError(error, this.logger); + }), + this.converter.pipeable(VERIFICATION_TOKEN_NORMALIZER) + ); + } } diff --git a/feature-libs/user/profile/root/facade/index.ts b/feature-libs/user/profile/root/facade/index.ts index 9d89f42a660..8f1256a2229 100644 --- a/feature-libs/user/profile/root/facade/index.ts +++ b/feature-libs/user/profile/root/facade/index.ts @@ -8,3 +8,4 @@ export * from './user-email.facade'; export * from './user-password.facade'; export * from './user-profile.facade'; export * from './user-register.facade'; +export * from './registration-verification-token.facade'; diff --git a/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts b/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts new file mode 100644 index 00000000000..ee8d5ccc071 --- /dev/null +++ b/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts @@ -0,0 +1,29 @@ +/* + * 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 '../../../account/root/feature-name'; +import { + RegistrationVerificationToken, + RegistrationVerificationTokenCreation, +} from '../model'; + +@Injectable({ + providedIn: 'root', + useFactory: () => + facadeFactory({ + facade: RegistrationVerificationTokenFacade, + feature: USER_ACCOUNT_CORE_FEATURE, + methods: ['createRegistrationVerificationToken'], + }), +}) +export abstract class RegistrationVerificationTokenFacade { + abstract createRegistrationVerificationToken( + registrationVerificationTokenCreation: RegistrationVerificationTokenCreation + ): Observable; +} diff --git a/feature-libs/user/profile/root/model/index.ts b/feature-libs/user/profile/root/model/index.ts index ef82bd3ada2..00e10341dbd 100644 --- a/feature-libs/user/profile/root/model/index.ts +++ b/feature-libs/user/profile/root/model/index.ts @@ -6,3 +6,4 @@ export * from './user-profile.model'; export * from './augmented.model'; +export * from './otp-registration.model'; diff --git a/feature-libs/user/profile/root/model/otp-registration.model.ts b/feature-libs/user/profile/root/model/otp-registration.model.ts new file mode 100644 index 00000000000..a0f6ccd6ad7 --- /dev/null +++ b/feature-libs/user/profile/root/model/otp-registration.model.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface RegistrationVerificationTokenCreation { + purpose: string; + loginId: string; +} + +export interface RegistrationVerificationToken { + expiresIn: string; + tokenId: string; +} + +export enum Registration_VERIFICATION_TOKEN_DIALOG_ACTION { + OK = 'OK', +} diff --git a/feature-libs/user/profile/root/model/user-profile.model.ts b/feature-libs/user/profile/root/model/user-profile.model.ts index 47369186e92..a760271c364 100644 --- a/feature-libs/user/profile/root/model/user-profile.model.ts +++ b/feature-libs/user/profile/root/model/user-profile.model.ts @@ -36,4 +36,6 @@ export interface UserSignUp { password?: string; titleCode?: string; uid?: string; + verificationTokenId?: string; + verificationTokenCode?: string; } diff --git a/feature-libs/user/profile/root/user-profile-root.module.ts b/feature-libs/user/profile/root/user-profile-root.module.ts index 83fe1104131..852ee56f8f0 100644 --- a/feature-libs/user/profile/root/user-profile-root.module.ts +++ b/feature-libs/user/profile/root/user-profile-root.module.ts @@ -30,6 +30,8 @@ export function defaultUserProfileComponentsConfig(): CmsConfig { 'ResetPasswordComponent', 'CloseAccountComponent', 'AccountAddressBookComponent', + 'ReturningCustomerOTPRegisterComponent', + 'VerifyOTPTokenForRegistrationComponent', ], }, // by default core is bundled together with components diff --git a/feature-libs/user/profile/styles/_index.scss b/feature-libs/user/profile/styles/_index.scss index be059edd6c7..f939dafced0 100644 --- a/feature-libs/user/profile/styles/_index.scss +++ b/feature-libs/user/profile/styles/_index.scss @@ -9,3 +9,5 @@ @import './address-book'; @import './address-form'; @import './suggested-addresses-dialog'; +@import './otp-register-form'; +@import './verification-token-form'; diff --git a/feature-libs/user/profile/styles/_otp-register-form.scss b/feature-libs/user/profile/styles/_otp-register-form.scss new file mode 100644 index 00000000000..5b3ac765543 --- /dev/null +++ b/feature-libs/user/profile/styles/_otp-register-form.scss @@ -0,0 +1,28 @@ +%cx-otp-register-form { + form { + a { + text-decoration: underline; + } + .cx-login-link { + margin: 1rem 0 0; + } + + .input-hint { + font-size: 0.875rem; + color: var(--cx-color-secondary); + margin-bottom: 0; + } + } + .cx-page-section { + padding-top: 5px; + } + .label-content { + font-family: 'Open Sans', sans-serif; + font-size: 16px; + font-weight: 600; + line-height: 21.79px; + text-align: left; + text-underline-position: from-font; + text-decoration-skip-ink: none; + } +} diff --git a/feature-libs/user/profile/styles/_verification-token-form.scss b/feature-libs/user/profile/styles/_verification-token-form.scss new file mode 100644 index 00000000000..2ec32385b66 --- /dev/null +++ b/feature-libs/user/profile/styles/_verification-token-form.scss @@ -0,0 +1,54 @@ +%cx-registration-verification-token-form { + .resend-link-text { + display: flex; + flex-direction: row; + width: 100%; + margin: auto; + + .left-text { + padding: 0; + width: 50%; + text-align: start; + } + + .right-text { + padding: 0; + width: 50%; + text-align: end; + } + + a.disabled-link { + pointer-events: none; + color: var(--cx-color-dark); + } + + a { + color: var(--cx-color-primary); + @include type(7); + } + } + + .verify-container { + width: 100%; + margin-top: 2.5rem; + } + + cx-spinner { + display: none; + } + + button { + flex: 100%; + } + + .label-content { + font-family: 'Open Sans', sans-serif; + font-size: 16px; + font-weight: 600; + line-height: 21.79px; + text-align: left; + text-underline-position: from-font; + text-decoration-skip-ink: none; + margin-top: 20px; + } +} diff --git a/projects/assets/src/translations/en/common.json b/projects/assets/src/translations/en/common.json index dabea7c8f7c..ac6d3f90f54 100644 --- a/projects/assets/src/translations/en/common.json +++ b/projects/assets/src/translations/en/common.json @@ -185,6 +185,7 @@ "cxMaxCharactersLength": "Password cannot have more than 128 characters", "cxContainsSpecialCharacters": "Password cannot contain special characters", "cxNoConsecutiveCharacters": "Password cannot contain consecutive identical characters", + "invalidTokenCodeError": "This code is not invalid", "date": { "required": "Field {{label}} is required", "min": "Field {{label}} cannot be before {{min}}", diff --git a/projects/storefrontlib/cms-structure/routing/default-routing-config.ts b/projects/storefrontlib/cms-structure/routing/default-routing-config.ts index 53e032c45d1..ba7f9fd4896 100644 --- a/projects/storefrontlib/cms-structure/routing/default-routing-config.ts +++ b/projects/storefrontlib/cms-structure/routing/default-routing-config.ts @@ -21,6 +21,11 @@ export const defaultStorefrontRoutesConfig: RoutesConfig = { protected: false, authFlow: true, }, + verifyTokenForRegistration: { + paths: ['register/verify-token'], + protected: false, + authFlow: true, + }, register: { paths: ['login/register'], protected: false, From 0adbc78a22e0f6bd4ac009c8d6e77fada1d48c33 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Jan 2025 02:56:50 +0000 Subject: [PATCH 02/15] Add license header --- feature-libs/user/profile/components/otp-login-register/index.ts | 1 + .../otp-login-register/otp-login-register.component.ts | 1 + .../components/otp-login-register/otp-login-register.module.ts | 1 + .../otp-login-register/otp-register-component.service.ts | 1 + .../components/registration-verification-token-form/index.ts | 1 + .../verify-register-verification-token-form.component.ts | 1 + .../verify-register-verification-token-form.module.ts | 1 + .../verify-register-verification-token-form.service.ts | 1 + feature-libs/user/profile/components/user-account-constants.ts | 1 + .../core/facade/registration-verification-token.service.ts | 1 + .../root/facade/registration-verification-token.facade.ts | 1 + feature-libs/user/profile/root/model/otp-registration.model.ts | 1 + 12 files changed, 12 insertions(+) diff --git a/feature-libs/user/profile/components/otp-login-register/index.ts b/feature-libs/user/profile/components/otp-login-register/index.ts index b71912923d6..4e1766fd57f 100644 --- a/feature-libs/user/profile/components/otp-login-register/index.ts +++ b/feature-libs/user/profile/components/otp-login-register/index.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts index 233589ed317..2ab22da9106 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts index e78a4995699..1a1fdb1b016 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts index 6c32858588e..bda080db33d 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/registration-verification-token-form/index.ts b/feature-libs/user/profile/components/registration-verification-token-form/index.ts index 2df8b8968c7..f30eb73454c 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/index.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/index.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts index 4e9513be952..c9bdeab2422 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts index ff341c818da..0bddb625ce4 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts index 6b000e0d213..97b9491e453 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/components/user-account-constants.ts b/feature-libs/user/profile/components/user-account-constants.ts index c690e9cf266..f8d519335c6 100644 --- a/feature-libs/user/profile/components/user-account-constants.ts +++ b/feature-libs/user/profile/components/user-account-constants.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/core/facade/registration-verification-token.service.ts b/feature-libs/user/profile/core/facade/registration-verification-token.service.ts index f535318d337..ef42788ce37 100644 --- a/feature-libs/user/profile/core/facade/registration-verification-token.service.ts +++ b/feature-libs/user/profile/core/facade/registration-verification-token.service.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts b/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts index ee8d5ccc071..3014ba276ab 100644 --- a/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts +++ b/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/feature-libs/user/profile/root/model/otp-registration.model.ts b/feature-libs/user/profile/root/model/otp-registration.model.ts index a0f6ccd6ad7..888234cec23 100644 --- a/feature-libs/user/profile/root/model/otp-registration.model.ts +++ b/feature-libs/user/profile/root/model/otp-registration.model.ts @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 */ From bbb760aead310806c8b83293fde570c47abc6f5a Mon Sep 17 00:00:00 2001 From: Melody-zhou-512 <140146828+Melody-zhou-512@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:06:15 +0800 Subject: [PATCH 03/15] adapt copyright adapt copyright --- .../user/profile/components/otp-login-register/index.ts | 1 - .../otp-login-register/otp-login-register.component.spec.ts | 5 +++++ .../otp-login-register/otp-login-register.component.ts | 1 - .../otp-login-register/otp-login-register.module.ts | 1 - .../otp-register-component.service.spec.ts | 5 +++++ .../otp-login-register/otp-register-component.service.ts | 1 - .../user/profile/components/register/register.component.html | 1 + .../components/registration-verification-token-form/index.ts | 1 - ...verify-register-verification-token-form.component.spec.ts | 5 +++++ .../verify-register-verification-token-form.component.ts | 1 - .../verify-register-verification-token-form.module.ts | 1 - .../verify-register-verification-token-form.service.spec.ts | 5 +++++ .../verify-register-verification-token-form.service.ts | 1 - .../user/profile/components/user-account-constants.ts | 1 - .../profile/core/connectors/user-profile.connector.spec.ts | 5 +++++ .../facade/registration-verification-token.service.spec.ts | 5 +++++ .../core/facade/registration-verification-token.service.ts | 1 - .../profile/occ/adapters/occ-user-profile.adapter.spec.ts | 5 +++++ .../root/facade/registration-verification-token.facade.ts | 1 - .../user/profile/root/model/otp-registration.model.ts | 1 - 20 files changed, 36 insertions(+), 12 deletions(-) diff --git a/feature-libs/user/profile/components/otp-login-register/index.ts b/feature-libs/user/profile/components/otp-login-register/index.ts index 4e1766fd57f..29cb1b18c8c 100644 --- a/feature-libs/user/profile/components/otp-login-register/index.ts +++ b/feature-libs/user/profile/components/otp-login-register/index.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts index 8e956accacf..34984c0511c 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ import { Component, Pipe, PipeTransform } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { AbstractControl, ReactiveFormsModule } from '@angular/forms'; diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts index 2ab22da9106..884f86f4584 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts index 1a1fdb1b016..9e5a382ccb9 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.module.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts index a99fd6d6d27..140fbeae7e4 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ import { inject, TestBed } from '@angular/core/testing'; import { UntypedFormBuilder } from '@angular/forms'; import { UserRegisterFacade } from '@spartacus/user/profile/root'; diff --git a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts index bda080db33d..fca2416b65a 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/register/register.component.html b/feature-libs/user/profile/components/register/register.component.html index 43bba667521..e931a943f37 100644 --- a/feature-libs/user/profile/components/register/register.component.html +++ b/feature-libs/user/profile/components/register/register.component.html @@ -10,6 +10,7 @@
+

{{ 'formLegend.required' | cxTranslate }}

diff --git a/feature-libs/user/profile/components/registration-verification-token-form/index.ts b/feature-libs/user/profile/components/registration-verification-token-form/index.ts index f30eb73454c..48c6a2893ef 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/index.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/index.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts index 98dec092861..1b528bf03eb 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ import { ChangeDetectorRef, Pipe, PipeTransform } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts index c9bdeab2422..37703e36309 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts index 0bddb625ce4..b7f581963db 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.module.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts index 870f833813b..47a0f252917 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ import { inject, TestBed } from '@angular/core/testing'; import { UntypedFormBuilder } from '@angular/forms'; import { diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts index 97b9491e453..1218d846401 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/components/user-account-constants.ts b/feature-libs/user/profile/components/user-account-constants.ts index f8d519335c6..ce0691639d0 100644 --- a/feature-libs/user/profile/components/user-account-constants.ts +++ b/feature-libs/user/profile/components/user-account-constants.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts b/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts index 1d522b5753a..9c275ff7b83 100644 --- a/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts +++ b/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ import { TestBed } from '@angular/core/testing'; import { RegistrationVerificationToken, diff --git a/feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts b/feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts index fd21b7b2b52..04594b73d3b 100644 --- a/feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts +++ b/feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ import { TestBed } from '@angular/core/testing'; import { CommandService } from '@spartacus/core'; diff --git a/feature-libs/user/profile/core/facade/registration-verification-token.service.ts b/feature-libs/user/profile/core/facade/registration-verification-token.service.ts index ef42788ce37..bba8ac9b920 100644 --- a/feature-libs/user/profile/core/facade/registration-verification-token.service.ts +++ b/feature-libs/user/profile/core/facade/registration-verification-token.service.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts index d102ddbd77e..68538b6aa1e 100644 --- a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts +++ b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ import { HttpClientTestingModule, HttpTestingController, diff --git a/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts b/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts index 3014ba276ab..08d6ceceb90 100644 --- a/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts +++ b/feature-libs/user/profile/root/facade/registration-verification-token.facade.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 diff --git a/feature-libs/user/profile/root/model/otp-registration.model.ts b/feature-libs/user/profile/root/model/otp-registration.model.ts index 888234cec23..7b8034b6c53 100644 --- a/feature-libs/user/profile/root/model/otp-registration.model.ts +++ b/feature-libs/user/profile/root/model/otp-registration.model.ts @@ -1,5 +1,4 @@ /* - * SPDX-FileCopyrightText: 2024 SAP Spartacus team * SPDX-FileCopyrightText: 2025 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 From 28a377327c93ae0a5f0c1c3beef88f2b2d0755fc Mon Sep 17 00:00:00 2001 From: Melody-zhou-512 <140146828+Melody-zhou-512@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:49:27 +0800 Subject: [PATCH 04/15] add e2e test add e2e test --- .../otp-registration.e2e-flaky.cy.ts | 174 ++++++++++++++++++ .../accessibility/tabbing-order.config.ts | 15 ++ .../cypress/helpers/register.ts | 2 +- 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-registration.e2e-flaky.cy.ts diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-registration.e2e-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-registration.e2e-flaky.cy.ts new file mode 100644 index 00000000000..6b6d9bcc0c3 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/user_access/otp-registration.e2e-flaky.cy.ts @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as login from '../../../helpers/login'; +import { viewportContext } from '../../../helpers/viewport-context'; +import { user } from '../../../sample-data/checkout-flow'; +import { interceptPost } from '../../../support/utils/intercept'; + +export function listenForCreateRegistrationVerificationToken(): string { + return interceptPost( + 'createRegistrationVerificationToken', + '/users/anonymous/verificationToken?*' + ); +} + +export function listenForUserRegistrationVerficationCodeEmailReceive( + customerEmail: string +) { + const mailCCV2Url = + Cypress.env('MAIL_CCV2_URL') + + Cypress.env('MAIL_CCV2_PREFIX') + + '/search?query=' + + customerEmail + + '&kind=to'; + + cy.request({ + method: 'GET', + url: mailCCV2Url, + }).then((response) => { + if (response.body.total != 1) { + listenForUserRegistrationVerficationCodeEmailReceive(customerEmail); + } + }); +} + +describe('OTP Registration', () => { + viewportContext(['mobile'], () => { + describe('Create OTP For Registration', () => { + beforeEach(() => { + cy.visit('/login/register'); + }); + + it('should be able to create a new OTP by customer click Continue button (CXSPA-3919)', () => { + listenForCreateRegistrationVerificationToken(); + + cy.log(`create verification token from the register form`); + cy.get('cx-otp-register-form form').within(() => { + cy.get('ng-select[formcontrolname="titleCode"]') + .click() + .get('div.ng-option') + .contains('Mr') + .click(); + cy.get('[formcontrolname="firstName"]').clear().type(user.firstName); + cy.get('[formcontrolname="lastName"]').clear().type(user.lastName); + cy.get('[formcontrolname="email"]').clear().type(user.email); + cy.get('[formcontrolname="termsandconditions"]').click(); + cy.get('button[type=submit]').click(); + }); + + cy.wait('@createRegistrationVerificationToken') + .its('response.statusCode') + .should('eq', 201); + + cy.get('cx-registration-verification-token-form').should('exist'); + cy.get('cx-registration-verification-token-form').should('be.visible'); + + listenForUserRegistrationVerficationCodeEmailReceive(user.email); + + const mailCCV2Url = + Cypress.env('MAIL_CCV2_URL') + + Cypress.env('MAIL_CCV2_PREFIX') + + '/search?query=' + + user.email + + '&kind=to&start=0&limit=1'; + + cy.request({ + method: 'GET', + url: mailCCV2Url, + }).then((response) => { + console.log('Response Status:', response.status); + console.log('Response Body:', response.body); + const verificationCodeEmailStartText = + 'Please use the following verification code to register in Spartacus Electronics Site:

'; + const lableP = '

'; + + const items = response.body.items; + const emailBody = items[0].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-registration-verification-token-form').within(() => { + cy.get('[formcontrolname="tokenCode"]') + .clear() + .type(verificationCode); + cy.get('[formcontrolname="password"]').clear().type(user.password); + cy.get('[formcontrolname="passwordconf"]') + .clear() + .type(user.password); + cy.get('button[type=submit]').click(); + }); + cy.get('cx-login').should('exist'); + }); + }); + it('should be able to create a new OTP by customer click Continue button (CXSPA-3919)', () => { + listenForCreateRegistrationVerificationToken(); + + cy.log(`create verification token from the register form`); + cy.get('cx-otp-register-form form').within(() => { + cy.get('ng-select[formcontrolname="titleCode"]') + .click() + .get('div.ng-option') + .contains('Mr') + .click(); + cy.get('[formcontrolname="firstName"]').clear().type(user.firstName); + cy.get('[formcontrolname="lastName"]').clear().type(user.lastName); + cy.get('[formcontrolname="email"]').clear().type(user.email); + cy.get('[formcontrolname="termsandconditions"]').click(); + cy.get('button[type=submit]').click(); + }); + + cy.wait('@createRegistrationVerificationToken') + .its('response.statusCode') + .should('eq', 201); + const verificationCode = 'invalidCode'; + + cy.get('cx-registration-verification-token-form').should('exist'); + cy.get('cx-registration-verification-token-form').should('be.visible'); + cy.get('cx-registration-verification-token-form').within(() => { + cy.get('[formcontrolname="tokenCode"]') + .clear() + .type(verificationCode); + cy.get('[formcontrolname="password"]').clear().type(user.password); + cy.get('[formcontrolname="passwordconf"]') + .clear() + .type(user.password); + cy.get('button[type=submit]').click(); + }); + cy.get('cx-form-errors') + .should('be.visible') + .and('contain.text', 'This code is not invalid'); + }); + }); + + describe('Verification token', () => { + beforeEach(() => { + cy.visit('/register/verify-token'); + }); + it('Should go back to register page when click back button (CXSPA-3919)', () => { + cy.get('cx-registration-verification-token-form').should('exist'); + + cy.get('cx-registration-verification-token-form form').within(() => { + cy.get('div.verify-container button').contains('Back').click(); + }); + + cy.get('cx-registration-verification-token-form').should('not.exist'); + cy.get('cx-otp-register-form form').should('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 65d3656ce19..bd3abd9624c 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 @@ -2551,4 +2551,19 @@ export const tabbingOrderConfig: TabbingOrderConfig = { { value: 'Verify', type: TabbingOrderTypes.BUTTON }, { value: 'Back', type: TabbingOrderTypes.LINK }, ], + otpRegistration: [ + { value: 'Title', type: TabbingOrderTypes.FORM_FIELD }, + { value: 'First name', type: TabbingOrderTypes.FORM_FIELD }, + { value: 'Last name', type: TabbingOrderTypes.FORM_FIELD }, + { value: 'Email address', type: TabbingOrderTypes.FORM_FIELD }, + { value: 'Continue', type: TabbingOrderTypes.BUTTON }, + ], + verifyTokenForRegistration: [ + { value: 'tokenCode', type: TabbingOrderTypes.FORM_FIELD }, + { value: "Didn't receive the code?", type: TabbingOrderTypes.LINK }, + { value: 'Register', type: TabbingOrderTypes.BUTTON }, + { value: 'Back', type: TabbingOrderTypes.LINK }, + { value: 'Password', type: TabbingOrderTypes.FORM_FIELD }, + { value: 'Confirm Password', type: TabbingOrderTypes.FORM_FIELD }, + ], }; diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/register.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/register.ts index 049e8290386..17af2fce418 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/register.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/register.ts @@ -28,7 +28,7 @@ export function verifyGlobalMessageAfterRegistration() { alert.should( 'contain', - 'Successful Registration: Please log in with provided credentials' + 'Your account has been successfully created! Please log in with provided credentials' ); cy.location().should((location) => { expect(location.pathname).to.match(/\/login$/); From 412f4fc263dfac1204977b17c303a385106cc5b2 Mon Sep 17 00:00:00 2001 From: Melody-zhou-512 <140146828+Melody-zhou-512@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:07:18 +0800 Subject: [PATCH 05/15] fix code review comments fix code review comments --- .../account/root/model/otp-login.model.ts | 2 +- .../account/root/user-account-root.module.ts | 2 +- .../assets/translations/en/userProfile.json | 2 +- .../components/otp-login-register/index.ts | 1 - .../otp-login-register.component.html | 2 +- .../otp-login-register.component.spec.ts | 46 ++++++------ .../otp-login-register.component.ts | 72 ++++++++---------- .../otp-login-register.module.ts | 14 ++-- .../otp-register-component.service.spec.ts | 61 ---------------- .../otp-register-component.service.ts | 49 ------------- ...-verification-token-form.component.spec.ts | 3 +- ...ister-verification-token-form.component.ts | 50 ++++++++----- ...er-verification-token-form.service.spec.ts | 16 +--- ...egister-verification-token-form.service.ts | 12 +-- .../user-profile-components.module.ts | 4 +- .../profile/core/connectors/converters.ts | 15 +--- .../core/connectors/user-profile.adapter.ts | 11 +-- .../connectors/user-profile.connector.spec.ts | 39 +--------- .../core/connectors/user-profile.connector.ts | 15 +--- .../profile/core/facade/facade-providers.ts | 8 +- .../user/profile/core/facade/index.ts | 1 - ...tration-verification-token.service.spec.ts | 73 ------------------- ...registration-verification-token.service.ts | 45 ------------ ...efault-occ-user-profile-endpoint.config.ts | 2 - .../config/occ-user-profile-endpoint.model.ts | 5 -- .../adapters/occ-user-profile.adapter.spec.ts | 57 +-------------- .../occ/adapters/occ-user-profile.adapter.ts | 41 +---------- .../user/profile/root/facade/index.ts | 1 - .../registration-verification-token.facade.ts | 29 -------- feature-libs/user/profile/root/model/index.ts | 1 - .../root/model/otp-registration.model.ts | 19 ----- .../profile/styles/_otp-register-form.scss | 6 -- .../styles/_verification-token-form.scss | 54 +++++++++++++- .../assets/src/translations/en/common.json | 2 +- ...-registration-tabbing.e2e-spec-flaky.cy.ts | 40 ++++++++++ .../otp-registration.e2e-flaky.cy.ts | 37 +++++----- .../accessibility/tabbing-order.config.ts | 26 ++++--- 37 files changed, 242 insertions(+), 621 deletions(-) delete mode 100644 feature-libs/user/profile/components/otp-login-register/otp-register-component.service.spec.ts delete mode 100644 feature-libs/user/profile/components/otp-login-register/otp-register-component.service.ts delete mode 100644 feature-libs/user/profile/core/facade/registration-verification-token.service.spec.ts delete mode 100644 feature-libs/user/profile/core/facade/registration-verification-token.service.ts delete mode 100644 feature-libs/user/profile/root/facade/registration-verification-token.facade.ts delete mode 100644 feature-libs/user/profile/root/model/otp-registration.model.ts create mode 100644 projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/otp-registration-tabbing.e2e-spec-flaky.cy.ts diff --git a/feature-libs/user/account/root/model/otp-login.model.ts b/feature-libs/user/account/root/model/otp-login.model.ts index fb26a114a51..b56e8eaba64 100644 --- a/feature-libs/user/account/root/model/otp-login.model.ts +++ b/feature-libs/user/account/root/model/otp-login.model.ts @@ -7,7 +7,7 @@ export interface VerificationTokenCreation { purpose: string; loginId: string; - password: string; + password?: string; } export interface VerificationToken { 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 559767b869e..f2446ece902 100644 --- a/feature-libs/user/account/root/user-account-root.module.ts +++ b/feature-libs/user/account/root/user-account-root.module.ts @@ -24,7 +24,7 @@ export function defaultUserAccountComponentsConfig(): CmsConfig { 'ReturningCustomerRegisterComponent', 'MyAccountViewUserComponent', 'ReturningCustomerOTPLoginComponent', - 'ReturningCustomerOTPRegistrationComponent', + 'ReturningCustomerOTPRegisterComponent', ], }, // by default core is bundled together with components diff --git a/feature-libs/user/profile/assets/translations/en/userProfile.json b/feature-libs/user/profile/assets/translations/en/userProfile.json index 401d950b12b..b5d5393acb8 100644 --- a/feature-libs/user/profile/assets/translations/en/userProfile.json +++ b/feature-libs/user/profile/assets/translations/en/userProfile.json @@ -32,7 +32,7 @@ "resetPassword": "Reset Password", "createAccount": "Create an account", "title": "Title", - "titleOptional": "Title", + "titleOptional": "Title (optional)", "firstName": { "label": "First name", "placeholder": "First name" diff --git a/feature-libs/user/profile/components/otp-login-register/index.ts b/feature-libs/user/profile/components/otp-login-register/index.ts index 29cb1b18c8c..29de7d62b68 100644 --- a/feature-libs/user/profile/components/otp-login-register/index.ts +++ b/feature-libs/user/profile/components/otp-login-register/index.ts @@ -6,4 +6,3 @@ export * from './otp-login-register.component'; export * from './otp-login-register.module'; -export * from './otp-register-component.service'; diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html index 080852cc6e0..a89dcc38dff 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.html @@ -14,7 +14,7 @@