diff --git a/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json b/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json
index 04fe4cb0136..987c2d9ed13 100644
--- a/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json
+++ b/feature-libs/organization/user-registration/assets/translations/en/userRegistration.json
@@ -5,6 +5,10 @@
"label": "Title (optional)",
"placeholder": "Title"
},
+ "titleCodeOnOTPForm": {
+ "label": "Title",
+ "placeholder": "Select Title"
+ },
"firstName": {
"label": "First name",
"placeholder": "First name"
@@ -25,38 +29,71 @@
"label": "City/Town (optional)",
"placeholder": "Please select City/Town"
},
+ "cityOnOTPForm": {
+ "label": "City/Town",
+ "placeholder": "Please select City/Town"
+ },
"country": {
"label": "Country (optional)",
"placeholder": "Select Country"
},
+ "countryOnOTPForm": {
+ "label": "Country",
+ "placeholder": "Select Country"
+ },
"state": {
"label": "State/Province (optional)",
"placeholder": "Select State/Province"
},
+ "stateOnOTPForm": {
+ "label": "State/Province",
+ "placeholder": "Select State/Province"
+ },
"postalCode": {
"label": "Zip/Postal code (optional)",
"placeholder": "Zip/Postal code"
},
+ "postalCodeOnOTPForm": {
+ "label": "Zip/Postal code",
+ "placeholder": "Zip/Postal code"
+ },
"addressLine": {
"label": "Address (optional)",
"placeholder": "Address"
},
+ "addressLineOnOTPForm": {
+ "label": "Address",
+ "placeholder": "Address"
+ },
"secondAddressLine": {
"label": "Address line 2 (optional)",
"placeholder": "Address line 2"
},
+ "secondAddressLineOnOTPForm": {
+ "label": "Address line 2",
+ "placeholder": "Address line 2"
+ },
"phoneNumber": {
"label": "Phone number (optional)",
"placeholder": "Phone number"
},
+ "phoneNumberOnOTPForm": {
+ "label": "Phone number",
+ "placeholder": "Phone number"
+ },
"message": {
"label": "Message (optional)",
"placeholder": "An example data for the message field: \"Department: Ground support; Position: Chief safe guard; Report to: Steve Jackson; Comments: Please create new account for me\"."
+ },
+ "messageOnOTPForm": {
+ "label": "Message",
+ "placeholder": "An example data for the message field: \"Department: Ground support; Position: Chief safe guard; Report to: Steve Jackson; Comments: Please create new account for me\"."
}
},
"messageToApproverTemplate": "Company name: {{companyName}},\n Phone number: {{phoneNumber}},\n Address: {{addressLine}} {{secondAddressLine}} {{city}} {{state}} {{postalCode}} {{country}},\n Message: {{message}}",
"successFormSubmitMessage": "Thank you for registering! A representative will contact you shortly and confirm your access information.",
"formSubmitButtonLabel": "Register",
+ "continueWithOTP": "Continue",
"goToLoginButtonLabel": "Already registered? Go to Sign in",
"httpHandlers": {
"conflict": "User with this e-mail address already exists."
diff --git a/feature-libs/organization/user-registration/components/public_api.ts b/feature-libs/organization/user-registration/components/public_api.ts
index 6ba919f1311..ac0b9ae58ad 100644
--- a/feature-libs/organization/user-registration/components/public_api.ts
+++ b/feature-libs/organization/user-registration/components/public_api.ts
@@ -6,3 +6,5 @@
export * from './user-registration-components.module';
export * from './form/index';
+export * from './registration-otp-form/index';
+export * from './verification-token-form/index';
diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/index.ts b/feature-libs/organization/user-registration/components/registration-otp-form/index.ts
new file mode 100644
index 00000000000..7aceb71b2fd
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/registration-otp-form/index.ts
@@ -0,0 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './user-registration-otp-form.component';
+export * from './user-registration-otp-form.module';
diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.html b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.html
new file mode 100644
index 00000000000..d74d1ba71c9
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.html
@@ -0,0 +1,422 @@
+
+
+
+
+
+
+
+ *
+
diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts
new file mode 100644
index 00000000000..b0264423838
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts
@@ -0,0 +1,266 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Observable, of } from 'rxjs';
+import {
+ Country,
+ GlobalMessageService,
+ GlobalMessageType,
+ I18nTestingModule,
+ Region,
+ RoutingService,
+ Title,
+ Translatable,
+} from '@spartacus/core';
+import { VerificationTokenFacade } from '@spartacus/user/account/root';
+import { UserRegistrationOTPFormComponent } from './user-registration-otp-form.component';
+import { UserRegistrationFormService } from '../form';
+import createSpy = jasmine.createSpy;
+import { Pipe, PipeTransform } from '@angular/core';
+import { NgSelectModule } from '@ng-select/ng-select';
+import {
+ FormErrorsModule,
+ NgSelectA11yDirective,
+ SpinnerComponent,
+} from '@spartacus/storefront';
+import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive';
+
+class MockRoutingService implements Partial {
+ go = () => Promise.resolve(true);
+}
+
+class MockGlobalMessageService implements Partial {
+ add(_: string | Translatable, __: GlobalMessageType, ___?: number): void {}
+}
+
+class MockVerificationTokenFacade implements Partial {
+ createVerificationToken = createSpy().and.returnValue(
+ of({ tokenId: 'testTokenId', expiresIn: '300' })
+ );
+}
+
+const mockForm: FormGroup = new FormGroup({
+ titleCode: new FormControl(),
+ firstName: new FormControl(),
+ lastName: new FormControl(),
+ companyName: new FormControl(),
+ email: new FormControl(),
+ country: new FormGroup({
+ isocode: new FormControl(),
+ }),
+ region: new FormGroup({
+ isocode: new FormControl(),
+ }),
+ town: new FormControl(),
+ line1: new FormControl(),
+ line2: new FormControl(),
+ postalCode: new FormControl(),
+ phoneNumber: new FormControl(),
+ message: new FormControl(),
+});
+
+const mockRegions: Region[] = [
+ {
+ isocode: 'CA-ON',
+ name: 'Ontario',
+ },
+ {
+ isocode: 'CA-QC',
+ name: 'Quebec',
+ },
+];
+
+const mockTitles: Title[] = [
+ {
+ code: '0002',
+ name: 'Mr.',
+ },
+ {
+ code: '0001',
+ name: 'Mrs.',
+ },
+];
+
+const mockCountries: Country[] = [
+ {
+ isocode: 'CA',
+ name: 'Canada',
+ },
+ {
+ isocode: 'PL',
+ name: 'Poland',
+ },
+];
+
+class MockUserRegistrationFormService
+ implements Partial
+{
+ getTitles(): Observable {
+ return of(mockTitles);
+ }
+
+ getCountries(): Observable {
+ return of(mockCountries);
+ }
+
+ getRegions(): Observable {
+ return of(mockRegions);
+ }
+
+ get form(): FormGroup {
+ return mockForm;
+ }
+}
+
+@Pipe({
+ name: 'cxUrl',
+})
+class MockUrlPipe implements PipeTransform {
+ transform() {}
+}
+describe('UserRegistrationOTPFormComponent', () => {
+ let component: UserRegistrationOTPFormComponent;
+ let fixture: ComponentFixture;
+ let verificationTokenFacade: VerificationTokenFacade;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ RouterTestingModule,
+ I18nTestingModule,
+ NgSelectModule,
+ FormErrorsModule,
+ ],
+ declarations: [
+ UserRegistrationOTPFormComponent,
+ MockUrlPipe,
+ NgSelectA11yDirective,
+ SpinnerComponent,
+ MockFeatureDirective,
+ ],
+ providers: [
+ { provide: RoutingService, useClass: MockRoutingService },
+ { provide: GlobalMessageService, useClass: MockGlobalMessageService },
+ {
+ provide: VerificationTokenFacade,
+ useClass: MockVerificationTokenFacade,
+ },
+ {
+ provide: UserRegistrationFormService,
+ useClass: MockUserRegistrationFormService,
+ },
+ ],
+ });
+ verificationTokenFacade = TestBed.inject(VerificationTokenFacade);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserRegistrationOTPFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create the component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should initialize form', () => {
+ expect(component.registerForm).toBeTruthy();
+ expect(component.registerForm.get('email')).toBeTruthy();
+ });
+
+ it('should not submit if form is invalid', () => {
+ component.registerForm.setValue({
+ titleCode: '0001',
+ firstName: 'John',
+ lastName: 'Doe',
+ companyName: 'Company',
+ email: '',
+ country: { isocode: 'CA' },
+ region: { isocode: 'CA-ON' },
+ town: 'Townsville',
+ line1: '123 Main St',
+ line2: '',
+ postalCode: '12345',
+ phoneNumber: '1234567890',
+ message: '',
+ });
+ component.onSubmit();
+ expect(
+ verificationTokenFacade.createVerificationToken
+ ).not.toHaveBeenCalled();
+ });
+
+ it('should submit form when valid', () => {
+ const verificationData = {
+ loginId: 'test@example.com',
+ purpose: 'REGISTRATION',
+ };
+ component.registerForm.setValue({
+ titleCode: '0001',
+ firstName: 'John',
+ lastName: 'Doe',
+ companyName: 'Company',
+ email: 'test@example.com',
+ country: { isocode: 'CA' },
+ region: { isocode: 'CA-ON' },
+ town: 'Townsville',
+ line1: '123 Main St',
+ line2: '',
+ postalCode: '12345',
+ phoneNumber: '1234567890',
+ message: '',
+ });
+ component.onSubmit();
+ expect(
+ verificationTokenFacade.createVerificationToken
+ ).toHaveBeenCalledWith(verificationData);
+ });
+
+ it('should mark all fields as touched if form is invalid', () => {
+ spyOn(component.registerForm, 'markAllAsTouched').and.callThrough();
+ component.registerForm.patchValue({
+ email: '',
+ });
+ component.onSubmit();
+ expect(component.registerForm.markAllAsTouched).toHaveBeenCalled();
+ expect(
+ verificationTokenFacade.createVerificationToken
+ ).not.toHaveBeenCalled();
+ });
+
+ it('should navigate to verifyTokenRegister route', () => {
+ const routingService = TestBed.inject(RoutingService);
+ spyOn(routingService, 'go').and.callThrough();
+ const verificationToken = { tokenId: 'testToken', expiresIn: '300' };
+ const formData = {
+ email: 'test@example.com',
+ titleCode: '0001',
+ firstName: 'John',
+ lastName: 'Doe',
+ companyName: 'Company',
+ country: { isocode: 'CA' },
+ region: { isocode: 'CA-ON' },
+ town: 'Townsville',
+ line1: '123 Main St',
+ line2: '',
+ postalCode: '12345',
+ phoneNumber: '1234567890',
+ message: '',
+ };
+ component.registerForm.setValue(formData);
+ component['goToVerificationTokenForm'](verificationToken);
+ expect(routingService.go).toHaveBeenCalledWith(
+ { cxRoute: 'verifyTokenForRegistration' },
+ {
+ state: {
+ registrationDataForm: formData,
+ loginId: 'test@example.com',
+ tokenId: 'testToken',
+ expiresIn: '300',
+ },
+ }
+ );
+ });
+});
diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.ts
new file mode 100644
index 00000000000..0cfbe6fb938
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import {
+ Country,
+ GlobalMessageService,
+ Region,
+ RoutingService,
+ WindowRef,
+} from '@spartacus/core';
+import { Title } from '@spartacus/user/profile/root';
+import { BehaviorSubject, Observable } from 'rxjs';
+import {
+ VerificationToken,
+ VerificationTokenCreation,
+ VerificationTokenFacade,
+} from '@spartacus/user/account/root';
+import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants';
+import { UserRegistrationFormService } from '../form';
+
+@Component({
+ selector: 'cx-user-registration-form',
+ templateUrl: './user-registration-otp-form.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UserRegistrationOTPFormComponent {
+ protected routingService = inject(RoutingService);
+ protected verificationTokenFacade = inject(VerificationTokenFacade);
+ protected winRef = inject(WindowRef);
+ protected globalMessageService = inject(GlobalMessageService, {
+ optional: true,
+ });
+ protected userRegistrationFormService = inject(UserRegistrationFormService);
+ protected busy$ = new BehaviorSubject(false);
+ titles$: Observable = this.userRegistrationFormService.getTitles();
+
+ countries$: Observable =
+ this.userRegistrationFormService.getCountries();
+
+ regions$: Observable =
+ this.userRegistrationFormService.getRegions();
+
+ registerForm: FormGroup = this.userRegistrationFormService.form;
+
+ isLoading$ = new BehaviorSubject(false);
+
+ onSubmit(): void {
+ if (!this.registerForm.valid) {
+ this.registerForm.markAllAsTouched();
+ return;
+ }
+
+ this.busy$.next(true);
+ const tokenCreationReqBody = this.collectDataFromRegistrationForm();
+ this.verificationTokenFacade
+ .createVerificationToken(tokenCreationReqBody)
+ .subscribe({
+ next: (result: VerificationToken) =>
+ this.goToVerificationTokenForm(result),
+ error: () => this.busy$.next(false),
+ complete: () => this.onCreateVerificationTokenComplete(),
+ });
+ }
+
+ protected goToVerificationTokenForm(
+ verificationToken: VerificationToken
+ ): void {
+ this.routingService.go(
+ {
+ cxRoute: 'verifyTokenForRegistration',
+ },
+ {
+ state: {
+ registrationDataForm: this.registerForm.value,
+ loginId: this.registerForm.value.email.toLowerCase(),
+ tokenId: verificationToken.tokenId,
+ expiresIn: verificationToken.expiresIn,
+ },
+ }
+ );
+ }
+
+ protected collectDataFromRegistrationForm(): VerificationTokenCreation {
+ return {
+ loginId: this.registerForm.value.email.toLowerCase(),
+ purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE,
+ };
+ }
+
+ protected onCreateVerificationTokenComplete(): void {
+ this.registerForm.reset();
+ this.busy$.next(false);
+ }
+}
diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.module.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.module.ts
new file mode 100644
index 00000000000..c141bc08fcc
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.module.ts
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+import { NgSelectModule } from '@ng-select/ng-select';
+import {
+ CmsConfig,
+ ConfigModule,
+ FeaturesConfigModule,
+ I18nModule,
+ NotAuthGuard,
+ UrlModule,
+} from '@spartacus/core';
+import {
+ FormErrorsModule,
+ NgSelectA11yModule,
+ SpinnerModule,
+} from '@spartacus/storefront';
+import { UserRegistrationOTPFormComponent } from './user-registration-otp-form.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ RouterModule,
+ UrlModule,
+ I18nModule,
+ SpinnerModule,
+ FormErrorsModule,
+ NgSelectModule,
+ NgSelectA11yModule,
+ ConfigModule.withConfig({
+ cmsComponents: {
+ RegisterB2BCustomerWithOTPComponent: {
+ component: UserRegistrationOTPFormComponent,
+ guards: [NotAuthGuard],
+ },
+ },
+ }),
+ FeaturesConfigModule,
+ ],
+ declarations: [UserRegistrationOTPFormComponent],
+ exports: [UserRegistrationOTPFormComponent],
+})
+export class UserRegistrationOTPFormModule {}
diff --git a/feature-libs/organization/user-registration/components/user-registration-components.module.ts b/feature-libs/organization/user-registration/components/user-registration-components.module.ts
index 9bb33ae90c8..6ec8b416649 100644
--- a/feature-libs/organization/user-registration/components/user-registration-components.module.ts
+++ b/feature-libs/organization/user-registration/components/user-registration-components.module.ts
@@ -7,8 +7,15 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { UserRegistrationFormModule } from './form/user-registration-form.module';
+import { UserRegistrationOTPFormModule } from './registration-otp-form/user-registration-otp-form.module';
+import { RegisterVerificationTokenFormModule } from './verification-token-form';
@NgModule({
- imports: [RouterModule, UserRegistrationFormModule],
+ imports: [
+ RouterModule,
+ UserRegistrationFormModule,
+ UserRegistrationOTPFormModule,
+ RegisterVerificationTokenFormModule,
+ ],
})
export class UserRegistrationComponentsModule {}
diff --git a/feature-libs/organization/user-registration/components/user-registration-constants.ts b/feature-libs/organization/user-registration/components/user-registration-constants.ts
new file mode 100644
index 00000000000..ce0691639d0
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/user-registration-constants.ts
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const ONE_TIME_PASSWORD_REGISTRATION_PURPOSE = 'REGISTRATION';
diff --git a/feature-libs/organization/user-registration/components/verification-token-form/index.ts b/feature-libs/organization/user-registration/components/verification-token-form/index.ts
new file mode 100644
index 00000000000..95c06e44791
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/verification-token-form/index.ts
@@ -0,0 +1,9 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './verification-token-form-component.service';
+export * from './verification-token-form.component';
+export * from './verification-token-form.module';
diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts
new file mode 100644
index 00000000000..f0ceddfd2d3
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts
@@ -0,0 +1,183 @@
+import { TestBed } from '@angular/core/testing';
+import { FormBuilder, UntypedFormGroup } from '@angular/forms';
+import {
+ AuthConfigService,
+ GlobalMessageService,
+ OAuthFlow,
+ RoutingService,
+ TranslationService,
+ GlobalMessageType,
+} from '@spartacus/core';
+import {
+ OrganizationUserRegistration,
+ UserRegistrationFacade,
+} from '@spartacus/organization/user-registration/root';
+import { of } from 'rxjs';
+import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service';
+
+class MockGlobalMessageService implements Partial {
+ add() {}
+}
+
+class MockOrganizationUserRegistrationFacade
+ implements Partial
+{
+ registerUser(userData: OrganizationUserRegistration) {
+ return of(userData);
+ }
+}
+
+class MockRoutingService implements Partial {
+ go = () => Promise.resolve(true);
+}
+
+class MockTranslationService implements Partial {
+ translate(value: string, options: any) {
+ return of(value + Object.values(options));
+ }
+}
+
+class MockAuthConfigService implements Partial {
+ getOAuthFlow() {
+ return OAuthFlow.ResourceOwnerPasswordFlow;
+ }
+}
+
+describe('RegisterVerificationTokenFormComponentService', () => {
+ let service: RegisterVerificationTokenFormComponentService;
+ let routingService: RoutingService;
+ let globalMessageService: GlobalMessageService;
+ let userRegistrationFacade: UserRegistrationFacade;
+ let translationService: TranslationService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ FormBuilder,
+ {
+ provide: RoutingService,
+ useClass: MockRoutingService,
+ },
+ {
+ provide: GlobalMessageService,
+ useClass: MockGlobalMessageService,
+ },
+ {
+ provide: AuthConfigService,
+ useClass: MockAuthConfigService,
+ },
+ {
+ provide: TranslationService,
+ useClass: MockTranslationService,
+ },
+ {
+ provide: UserRegistrationFacade,
+ useClass: MockOrganizationUserRegistrationFacade,
+ },
+ ],
+ });
+ service = TestBed.inject(RegisterVerificationTokenFormComponentService);
+ routingService = TestBed.inject(RoutingService);
+ globalMessageService = TestBed.inject(GlobalMessageService);
+ userRegistrationFacade = TestBed.inject(UserRegistrationFacade);
+ translationService = TestBed.inject(TranslationService);
+ });
+
+ it('should inject service', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should get the form', () => {
+ expect(service.form).toBeInstanceOf(UntypedFormGroup);
+ });
+
+ it('should redirect to login page', () => {
+ spyOn(routingService, 'go').and.callThrough();
+
+ service.registerUser(service.form.value).subscribe().unsubscribe();
+
+ expect(routingService.go).toHaveBeenCalledWith({
+ cxRoute: 'login',
+ });
+ });
+
+ it('should display a success message after registration', () => {
+ spyOn(globalMessageService, 'add');
+
+ service.registerUser(service.form.value).subscribe().unsubscribe();
+
+ expect(globalMessageService.add).toHaveBeenCalledWith(
+ {
+ key: 'userRegistrationForm.successFormSubmitMessage',
+ params: {},
+ },
+ GlobalMessageType.MSG_TYPE_CONFIRMATION,
+ 10000
+ );
+ });
+
+ it('should call buildMessageContent and translate correctly', () => {
+ spyOn(translationService, 'translate').and.callThrough();
+
+ const formValue = {
+ phoneNumber: '123456789',
+ line1: 'Address Line 1',
+ line2: 'Address Line 2',
+ city: 'City',
+ region: { isocode: 'RegionCode' },
+ postalCode: '12345',
+ country: { isocode: 'CountryCode' },
+ companyName: 'Company',
+ message: 'Message content',
+ };
+
+ const formValueAfterBind = {
+ phoneNumber: '123456789',
+ addressLine: 'Address Line 1',
+ secondAddressLine: 'Address Line 2',
+ city: 'City',
+ state: 'RegionCode',
+ postalCode: '12345',
+ country: 'CountryCode',
+ companyName: 'Company',
+ message: 'Message content',
+ };
+
+ service['buildMessageContent'](formValue).subscribe((result) => {
+ expect(translationService.translate).toHaveBeenCalledWith(
+ 'userRegistrationForm.messageToApproverTemplate',
+ formValueAfterBind
+ );
+ expect(result).toContain('123456789');
+ });
+ });
+
+ it('should call registerUser with correct data', () => {
+ spyOn(userRegistrationFacade, 'registerUser').and.callThrough();
+
+ service.form.setValue({
+ tokenId: 'testTokenId',
+ tokenCode: 'testTokenCode',
+ });
+
+ const formValue = {
+ titleCode: 'mr',
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john.doe@example.com',
+ phoneNumber: '123456789',
+ addressLine: 'Address Line 1',
+ secondAddressLine: 'Address Line 2',
+ city: 'City',
+ region: { isocode: 'RegionCode' },
+ postalCode: '12345',
+ country: { isocode: 'CountryCode' },
+ companyName: 'Company',
+ message: 'Message content',
+ };
+
+ service.registerUser(formValue).subscribe();
+
+ expect(userRegistrationFacade.registerUser).toHaveBeenCalled();
+ });
+});
diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.ts
new file mode 100644
index 00000000000..b39bc0283b5
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.ts
@@ -0,0 +1,117 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { inject, Injectable } from '@angular/core';
+import {
+ FormBuilder,
+ UntypedFormControl,
+ UntypedFormGroup,
+ Validators,
+} from '@angular/forms';
+import {
+ AuthConfigService,
+ GlobalMessageService,
+ GlobalMessageType,
+ OAuthFlow,
+ RoutingService,
+ TranslationService,
+} from '@spartacus/core';
+import {
+ OrganizationUserRegistration,
+ UserRegistrationFacade,
+} from '@spartacus/organization/user-registration/root';
+import { Observable } from 'rxjs';
+import { switchMap, take, tap } from 'rxjs/operators';
+
+const globalMsgShowTime: number = 10000;
+@Injectable({
+ providedIn: 'root',
+})
+export class RegisterVerificationTokenFormComponentService {
+ protected globalMessage: GlobalMessageService = inject(GlobalMessageService);
+ protected formBuilder = inject(FormBuilder);
+ protected organizationUserRegistrationFacade: UserRegistrationFacade = inject(
+ UserRegistrationFacade
+ );
+ protected translationService: TranslationService = inject(TranslationService);
+ protected authConfigService: AuthConfigService = inject(AuthConfigService);
+ protected routingService: RoutingService = inject(RoutingService);
+
+ form: UntypedFormGroup = new UntypedFormGroup({
+ tokenId: new UntypedFormControl('', [Validators.required]),
+ tokenCode: new UntypedFormControl('', [Validators.required]),
+ });
+
+ protected buildMessageContent(formValue: {
+ [key: string]: any;
+ }): Observable {
+ return this.translationService.translate(
+ 'userRegistrationForm.messageToApproverTemplate',
+ {
+ phoneNumber: formValue.phoneNumber,
+ addressLine: formValue.line1,
+ secondAddressLine: formValue.line2,
+ city: formValue.city,
+ state: formValue.region?.isocode,
+ postalCode: formValue.postalCode,
+ country: formValue.country?.isocode,
+ companyName: formValue.companyName,
+ message: formValue.message,
+ }
+ );
+ }
+
+ /**
+ * Redirects the user back to the login page.
+ *
+ * This only happens in case of the `ResourceOwnerPasswordFlow` OAuth flow.
+ */
+ protected redirectToLogin(): void {
+ if (
+ this.authConfigService.getOAuthFlow() ===
+ OAuthFlow.ResourceOwnerPasswordFlow
+ ) {
+ this.routingService.go({ cxRoute: 'login' });
+ }
+ }
+
+ displayMessage(key: string, params: Object) {
+ this.globalMessage.add(
+ {
+ key: key,
+ params,
+ },
+ GlobalMessageType.MSG_TYPE_CONFIRMATION,
+ globalMsgShowTime
+ );
+ }
+
+ registerUser(formValue: {
+ [key: string]: any;
+ }): Observable {
+ return this.buildMessageContent(formValue).pipe(
+ take(1),
+ switchMap((message: string) =>
+ this.organizationUserRegistrationFacade.registerUser({
+ titleCode: formValue.titleCode,
+ firstName: formValue.firstName,
+ lastName: formValue.lastName,
+ email: formValue.email,
+ message: message,
+ verificationTokenId: this.form.get('tokenId')?.value,
+ verificationTokenCode: this.form.get('tokenCode')?.value,
+ })
+ ),
+ tap(() => {
+ this.displayMessage(
+ 'userRegistrationForm.successFormSubmitMessage',
+ {}
+ );
+ this.redirectToLogin();
+ })
+ );
+ }
+}
diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.html b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.html
new file mode 100644
index 00000000000..3931ac8ac86
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+ *
+
diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts
new file mode 100644
index 00000000000..34ba0dc369e
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts
@@ -0,0 +1,241 @@
+import {
+ ChangeDetectorRef,
+ DebugElement,
+ Pipe,
+ PipeTransform,
+} from '@angular/core';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import {
+ ReactiveFormsModule,
+ UntypedFormControl,
+ UntypedFormGroup,
+} from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
+import { I18nTestingModule, RoutingService } from '@spartacus/core';
+import {
+ FormErrorsModule,
+ LaunchDialogService,
+ SpinnerModule,
+} from '@spartacus/storefront';
+import { BehaviorSubject, of } from 'rxjs';
+import createSpy = jasmine.createSpy;
+import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service';
+import { RegisterVerificationTokenFormComponent } from './verification-token-form.component';
+import { Store } from '@ngrx/store';
+import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants';
+import { VerificationTokenFacade } from '@spartacus/user/account/root';
+const isBusySubject = new BehaviorSubject(false);
+
+class MockFormComponentService
+ implements Partial
+{
+ form: UntypedFormGroup = new UntypedFormGroup({
+ tokenId: new UntypedFormControl(),
+ tokenCode: new UntypedFormControl(),
+ });
+ login = createSpy().and.stub();
+ createVerificationToken = createSpy().and.returnValue(
+ of({ tokenId: 'testTokenId', expiresIn: '300' })
+ );
+ displayMessage = createSpy('displayMessage').and.stub();
+}
+
+class MockRoutingService {
+ go = createSpy();
+}
+class MockStore {
+ dispatch = jasmine.createSpy();
+ select = jasmine.createSpy().and.returnValue(of({}));
+}
+@Pipe({
+ name: 'cxUrl',
+})
+class MockUrlPipe implements PipeTransform {
+ transform() {}
+}
+
+class MockLaunchDialogService implements Partial {
+ openDialogAndSubscribe = createSpy().and.stub();
+}
+
+describe('RegisterVerificationTokenFormComponent', () => {
+ let component: RegisterVerificationTokenFormComponent;
+ let fixture: ComponentFixture;
+ let el: DebugElement;
+ let service: RegisterVerificationTokenFormComponentService;
+ let facade: VerificationTokenFacade;
+ let launchDialogService: LaunchDialogService;
+ let routineservice: RoutingService;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ RouterTestingModule,
+ I18nTestingModule,
+ FormErrorsModule,
+ SpinnerModule,
+ ],
+ declarations: [RegisterVerificationTokenFormComponent, MockUrlPipe],
+ providers: [
+ { provide: Store, useClass: MockStore },
+ {
+ provide: RegisterVerificationTokenFormComponent,
+ useClass: MockFormComponentService,
+ },
+ {
+ provide: LaunchDialogService,
+ useClass: MockLaunchDialogService,
+ },
+ {
+ provide: RoutingService,
+ useClass: MockRoutingService,
+ },
+ ChangeDetectorRef,
+ ],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RegisterVerificationTokenFormComponent);
+ service = TestBed.inject(RegisterVerificationTokenFormComponentService);
+ facade = TestBed.inject(VerificationTokenFacade);
+ launchDialogService = TestBed.inject(LaunchDialogService);
+ routineservice = TestBed.inject(RoutingService);
+ component = fixture.componentInstance;
+ component.isUpdating$ = isBusySubject;
+ el = fixture.debugElement;
+ fixture.detectChanges();
+ history.pushState(
+ {
+ tokenId: '',
+ password: 'pw4all',
+ loginId: 'test@sap.com',
+ },
+ ''
+ );
+ });
+
+ it('should create component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('busy', () => {
+ it('should disable the submit button when form is disabled', () => {
+ component.form.disable();
+ fixture.detectChanges();
+ const submitBtn: HTMLButtonElement = el.query(
+ By.css('button')
+ ).nativeElement;
+ expect(submitBtn.disabled).toBeTruthy();
+ });
+
+ it('should show the spinner', () => {
+ isBusySubject.next(true);
+ fixture.detectChanges();
+ expect(el.query(By.css('cx-spinner'))).toBeTruthy();
+ });
+ });
+
+ describe('idle', () => {
+ it('should enable the submit button', () => {
+ component.form.enable();
+ fixture.detectChanges();
+ const submitBtn = el.query(By.css('button'));
+ expect(submitBtn.nativeElement.disabled).toBeFalsy();
+ });
+
+ it('should not show the spinner', () => {
+ isBusySubject.next(false);
+ fixture.detectChanges();
+ expect(el.query(By.css('cx-spinner'))).toBeNull();
+ });
+ });
+
+ describe('refresh with no tokenId/loginId', () => {
+ it('should navigate back to login page', () => {
+ spyOn(service, 'displayMessage');
+ history.pushState(
+ {
+ tokenId: '',
+ loginId: '',
+ },
+ ''
+ );
+ component.ngOnInit();
+ expect(routineservice.go).toHaveBeenCalledWith(['/login/register']);
+ expect(service.displayMessage).toHaveBeenCalledWith(
+ 'verificationTokenForm.needInputCredentials',
+ {}
+ );
+ });
+ });
+
+ describe('Form Interactions', () => {
+ it('should call onSubmit() method on submit', () => {
+ const request = spyOn(component, 'onSubmit');
+ const form = el.query(By.css('form'));
+ form.triggerEventHandler('submit', null);
+ expect(request).toHaveBeenCalled();
+ });
+
+ it('should call the service method on submit', () => {
+ spyOn(service, 'registerUser').and.returnValue(
+ of({
+ email: 'test@example.com',
+ firstName: 'John',
+ lastName: 'Doe',
+ titleCode: 'Mr',
+ password: 'Password123',
+ })
+ );
+ component.form.setValue({
+ tokenId: 'tokenId',
+ tokenCode: '123',
+ });
+ component.onSubmit();
+ expect(service.registerUser).toHaveBeenCalledWith(component.registerData);
+ });
+
+ it('should display info dialog', () => {
+ component.openInfoDailog();
+ expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled();
+ });
+
+ it('should display info dialog when keydown', () => {
+ const event = {
+ key: 'Enter',
+ preventDefault: () => {},
+ };
+ component.onOpenInfoDailogKeyDown(event as KeyboardEvent);
+ expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled();
+ });
+
+ it('should resend OTP', () => {
+ component.target = 'example@example.com';
+ spyOn(component, 'startWaitTimeInterval');
+ spyOn(service, 'displayMessage');
+ spyOn(facade, 'createVerificationToken').and.returnValue(
+ of({
+ tokenId: 'tokenId',
+ expiresIn: '300',
+ })
+ );
+
+ component.resendOTP();
+
+ expect(component.isResendDisabled).toBe(true);
+ expect(component.waitTime).toBe(60);
+ expect(component.startWaitTimeInterval).toHaveBeenCalled();
+ expect(facade.createVerificationToken).toHaveBeenCalledWith({
+ loginId: 'example@example.com',
+ purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE,
+ });
+ expect(service.displayMessage).toHaveBeenCalledWith(
+ 'verificationTokenForm.createVerificationToken',
+ { target: 'example@example.com' }
+ );
+ });
+ });
+});
diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.ts
new file mode 100644
index 00000000000..f7b09b38c37
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.ts
@@ -0,0 +1,156 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ HostBinding,
+ OnInit,
+ ViewChild,
+ inject,
+} from '@angular/core';
+import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront';
+
+import {
+ VerificationToken,
+ VerificationTokenFacade,
+} from '@spartacus/user/account/root';
+import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service';
+import { RoutingService } from '@spartacus/core';
+import { UntypedFormGroup } from '@angular/forms';
+import { Subject, Subscription } from 'rxjs';
+import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants';
+
+@Component({
+ selector: 'cx-verification-token-form',
+ templateUrl: './verification-token-form.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RegisterVerificationTokenFormComponent implements OnInit {
+ protected service: RegisterVerificationTokenFormComponentService = inject(
+ RegisterVerificationTokenFormComponentService
+ );
+ protected launchDialogService: LaunchDialogService =
+ inject(LaunchDialogService);
+ protected cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
+ protected routingService: RoutingService = inject(RoutingService);
+ protected verificationTokenFacade = inject(VerificationTokenFacade);
+ form: UntypedFormGroup = this.service.form;
+ isUpdating$: Subject = new Subject();
+ waitTime: number = 60;
+ protected subscriptions = new Subscription();
+
+ @HostBinding('class.user-form') style = true;
+
+ @ViewChild('noReceiveCodeLink') element: ElementRef;
+
+ @ViewChild('resendLink') resendLink: ElementRef;
+
+ tokenId: string;
+
+ registerData: UntypedFormGroup;
+
+ target: string;
+
+ isResendDisabled: boolean = true;
+
+ ngOnInit() {
+ if (!!history.state) {
+ this.registerData = history.state['registrationDataForm'];
+ this.tokenId = history.state['tokenId'];
+ this.target = history.state['loginId'];
+ history.pushState(
+ {
+ tokenId: '',
+ loginId: '',
+ },
+ 'verifyToken'
+ );
+ if (!this.target || !this.tokenId) {
+ this.service.displayMessage(
+ 'verificationTokenForm.needInputCredentials',
+ {}
+ );
+ this.routingService.go(['/login/register']);
+ } else {
+ this.startWaitTimeInterval();
+ this.service.displayMessage(
+ 'verificationTokenForm.createVerificationToken',
+ { target: this.target }
+ );
+ }
+ }
+ }
+
+ onSubmit(): void {
+ if (!this.form.valid) {
+ this.form.markAllAsTouched();
+ return;
+ }
+ this.isUpdating$.next(true);
+ this.subscriptions.add(
+ this.service.registerUser(this.registerData).subscribe({
+ complete: () => this.isUpdating$.next(false),
+ error: () => {
+ this.isUpdating$.next(false);
+ this.form
+ .get('tokenCode')
+ ?.setErrors({ invalidTokenCodeError: true });
+ },
+ })
+ );
+ }
+
+ resendOTP(): void {
+ this.isResendDisabled = true;
+ this.waitTime = 60;
+ this.resendLink.nativeElement.tabIndex = -1;
+ this.resendLink.nativeElement.blur();
+ this.startWaitTimeInterval();
+ this.verificationTokenFacade
+ .createVerificationToken({
+ loginId: this.target,
+ purpose: ONE_TIME_PASSWORD_REGISTRATION_PURPOSE,
+ })
+ .subscribe({
+ next: (result: VerificationToken) => (this.tokenId = result.tokenId),
+ complete: () =>
+ this.service.displayMessage(
+ 'verificationTokenForm.createVerificationToken',
+ { target: this.target }
+ ),
+ });
+ }
+
+ 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/organization/user-registration/components/verification-token-form/verification-token-form.module.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.module.ts
new file mode 100644
index 00000000000..9468de1ac66
--- /dev/null
+++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.module.ts
@@ -0,0 +1,71 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+import {
+ AuthService,
+ CmsConfig,
+ FeaturesConfigModule,
+ GlobalMessageService,
+ I18nModule,
+ NotAuthGuard,
+ UrlModule,
+ WindowRef,
+ provideDefaultConfig,
+} from '@spartacus/core';
+import {
+ FormErrorsModule,
+ IconModule,
+ KeyboardFocusModule,
+ SpinnerModule,
+} from '@spartacus/storefront';
+
+import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service';
+import { RegisterVerificationTokenFormComponent } from './verification-token-form.component';
+import { VerificationTokenFacade } from '@spartacus/user/account/root';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ KeyboardFocusModule,
+ ReactiveFormsModule,
+ RouterModule,
+ UrlModule,
+ IconModule,
+ I18nModule,
+ FormErrorsModule,
+ SpinnerModule,
+ FeaturesConfigModule,
+ ],
+ providers: [
+ provideDefaultConfig({
+ cmsComponents: {
+ VerifyOTPForB2BRegistrationComponent: {
+ component: RegisterVerificationTokenFormComponent,
+ guards: [NotAuthGuard],
+ providers: [
+ {
+ provide: RegisterVerificationTokenFormComponentService,
+ useClass: RegisterVerificationTokenFormComponentService,
+ deps: [
+ AuthService,
+ GlobalMessageService,
+ VerificationTokenFacade,
+ WindowRef,
+ ],
+ },
+ ],
+ },
+ },
+ }),
+ ],
+ declarations: [RegisterVerificationTokenFormComponent],
+})
+export class RegisterVerificationTokenFormModule {}
diff --git a/feature-libs/organization/user-registration/root/model/user-registration.model.ts b/feature-libs/organization/user-registration/root/model/user-registration.model.ts
index 5fb9650b2e6..1f44cfd9bc9 100644
--- a/feature-libs/organization/user-registration/root/model/user-registration.model.ts
+++ b/feature-libs/organization/user-registration/root/model/user-registration.model.ts
@@ -10,6 +10,8 @@ export interface OrganizationUserRegistration {
firstName: string;
lastName: string;
message?: string;
+ verificationTokenId?: string;
+ verificationTokenCode?: string;
}
export interface OrganizationUserRegistrationForm
diff --git a/feature-libs/organization/user-registration/root/user-registration-root.module.ts b/feature-libs/organization/user-registration/root/user-registration-root.module.ts
index 28619546527..23fe9a8eb9d 100644
--- a/feature-libs/organization/user-registration/root/user-registration-root.module.ts
+++ b/feature-libs/organization/user-registration/root/user-registration-root.module.ts
@@ -12,7 +12,11 @@ export function defaultOrganizationUserRegistrationComponentsConfig(): CmsConfig
const config: CmsConfig = {
featureModules: {
[ORGANIZATION_USER_REGISTRATION_FEATURE]: {
- cmsComponents: ['OrganizationUserRegistrationComponent'],
+ cmsComponents: [
+ 'OrganizationUserRegistrationComponent',
+ 'RegisterB2BCustomerWithOTPComponent',
+ 'VerifyOTPForB2BRegistrationComponent',
+ ],
},
},
};
diff --git a/feature-libs/user/account/assets/translations/en/userAccount.json b/feature-libs/user/account/assets/translations/en/userAccount.json
index 6f7c29744c1..92946757424 100644
--- a/feature-libs/user/account/assets/translations/en/userAccount.json
+++ b/feature-libs/user/account/assets/translations/en/userAccount.json
@@ -26,6 +26,7 @@
},
"noReceiveCode": "Didn't receive the code?",
"verify": "Verify",
+ "registerVerify": "Register",
"back": "Back",
"tokenInputHint": "You can request a new code. A timer starts indicating the seconds left until you can resend your request."
},
diff --git a/feature-libs/user/account/styles/_verification-token-form.scss b/feature-libs/user/account/styles/_verification-token-form.scss
index d9787db7708..fc1b896f5d0 100644
--- a/feature-libs/user/account/styles/_verification-token-form.scss
+++ b/feature-libs/user/account/styles/_verification-token-form.scss
@@ -29,6 +29,10 @@
}
}
+ .register-b2b-otp-resend-link-text {
+ margin-top: -1rem;
+ }
+
.verify-container {
width: 100%;
margin-top: 2.5rem;
diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-e2e-spec-flaky.cy.ts
new file mode 100644
index 00000000000..377440f7421
--- /dev/null
+++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-e2e-spec-flaky.cy.ts
@@ -0,0 +1,96 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { listenForTokenAuthenticationRequest } from '../../../../helpers/login';
+
+describe('Tabbing order for B2B OTP registration', () => {
+ before(() => {
+ cy.window().then((win) => win.sessionStorage.clear());
+ });
+
+ describe('B2B OTP Registration', () => {
+ context('B2B OTP Registration page', () => {
+ beforeEach(() => {
+ cy.visit('/login/register');
+ });
+ it('should allow to navigate with tab key for otp registration form and otp verification page(CXSPA-8772)', () => {
+ cy.get('cx-user-registration-form').should('exist');
+ cy.get('[formcontrolname="titleCode"]').ngSelect('Mr.');
+ cy.get('[formcontrolname="firstName"]').type('John');
+ cy.get('[formcontrolname="lastName"]').type('Doe');
+ cy.get('[formcontrolname="companyName"]').type('My Company Inc.');
+ var email =
+ 'test.test' + Math.floor(Math.random() * 10001) + '@sap.com';
+ cy.get('[formcontrolname="email"]').type(email);
+
+ cy.get('button[type=submit]').click();
+ cy.get('cx-verification-token-form').should('exist');
+ listenForUserVerficationCodeEmailReceive(email);
+ const mailCCV2Url =
+ Cypress.env('MAIL_CCV2_URL') +
+ Cypress.env('MAIL_CCV2_PREFIX') +
+ '/search?query=' +
+ email +
+ '&kind=to&start=0&limit=1';
+
+ cy.request({
+ method: 'GET',
+ url: mailCCV2Url,
+ }).then((response) => {
+ const verificationCodeEmailStartText =
+ 'Please use the following verification code to register in Spartacus powertools 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
+ );
+
+ listenForTokenAuthenticationRequest();
+ cy.get('cx-verification-token-form').within(() => {
+ cy.get('[formcontrolname="tokenCode"]')
+ .clear()
+ .type(verificationCode);
+ cy.get('button[type=submit]').click();
+ });
+ cy.get('cx-global-message').should('exist');
+ cy.get('cx-global-message').contains(
+ 'Thank you for registering! A representative will contact you shortly and confirm your access information.'
+ );
+ cy.get('cx-login').should('exist');
+ });
+ });
+ });
+ });
+});
+
+export function listenForUserVerficationCodeEmailReceive(
+ 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) {
+ listenForUserVerficationCodeEmailReceive(customerEmail);
+ }
+ });
+}
diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-tabbing-e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-tabbing-e2e-spec-flaky.cy.ts
new file mode 100644
index 00000000000..7d18f83cea8
--- /dev/null
+++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/b2b/regression/user-registration/b2b-otp-registration-tabbing-e2e-spec-flaky.cy.ts
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ verifyTabbingOrderForRegistrationWithOTP,
+ verifyTabbingOrderForOTPVerification,
+} from '../../../../helpers/b2b/b2b-user-registration';
+
+describe('Tabbing order for B2B OTP registration', () => {
+ before(() => {
+ cy.window().then((win) => win.sessionStorage.clear());
+ });
+
+ describe('B2B OTP Registration', () => {
+ context('B2B OTP Registration page', () => {
+ beforeEach(() => {
+ cy.visit('/login/register');
+ });
+ it('should allow to navigate with tab key for otp registration form and otp verification page(CXSPA-8772)', () => {
+ cy.get('cx-user-registration-form').should('exist');
+ cy.get('[formcontrolname="titleCode"]').ngSelect('Mr.');
+ cy.get('[formcontrolname="firstName"]').type('John');
+ cy.get('[formcontrolname="lastName"]').type('Doe');
+ cy.get('[formcontrolname="companyName"]').type('My Company Inc.');
+ cy.get('[formcontrolname="email"]').type('test.test@sap.com');
+ verifyTabbingOrderForRegistrationWithOTP();
+
+ cy.get('button[type=submit]').click();
+ cy.get('cx-verification-token-form').should('exist');
+ verifyTabbingOrderForOTPVerification();
+ });
+ });
+ });
+});
diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts
index 63ab1d283ee..0aa85b6ca58 100644
--- a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts
+++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/b2b/tabbing-order.config.ts
@@ -499,6 +499,36 @@ export const tabbingOrderConfig: TabbingOrderConfig = {
type: TabbingOrderTypes.LINK,
},
],
+ userRegistrationFormWithOTP: [
+ { type: TabbingOrderTypes.NG_SELECT },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.NG_SELECT },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.TEXT_AREA },
+ {
+ value: 'Continue',
+ type: TabbingOrderTypes.BUTTON,
+ },
+ ],
+ verificationToken: [
+ { type: TabbingOrderTypes.FORM_FIELD },
+ { type: TabbingOrderTypes.BUTTON },
+ {
+ value: 'Register',
+ type: TabbingOrderTypes.BUTTON,
+ },
+ {
+ value: 'Back',
+ type: TabbingOrderTypes.BUTTON,
+ },
+ ],
quoteDetailsPage: [
{
value: 'New Cart',
diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts
index 04701f45eca..c4b53ea762f 100644
--- a/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts
+++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/b2b/b2b-user-registration.ts
@@ -105,6 +105,14 @@ export function verifyTabbingOrder() {
);
}
+export function verifyTabbingOrderForRegistrationWithOTP() {
+ tabbingOrder('form', config.userRegistrationFormWithOTP);
+}
+
+export function verifyTabbingOrderForOTPVerification() {
+ tabbingOrder('form', config.verificationToken);
+}
+
export function verifyFormErrors() {
const requiredFirstNameFieldMessage = 'Field First name is required';
const requiredLastNameFieldMessage = 'Field Last name is required';