diff --git a/docs/migration/2211/2211-html.md b/docs/migration/2211_0/2211-html.md similarity index 100% rename from docs/migration/2211/2211-html.md rename to docs/migration/2211_0/2211-html.md diff --git a/docs/migration/2211/2211-installation.md b/docs/migration/2211_0/2211-installation.md similarity index 100% rename from docs/migration/2211/2211-installation.md rename to docs/migration/2211_0/2211-installation.md diff --git a/docs/migration/2211/2211-migration.md b/docs/migration/2211_0/2211-migration.md similarity index 100% rename from docs/migration/2211/2211-migration.md rename to docs/migration/2211_0/2211-migration.md diff --git a/docs/migration/2211/2211-styling.md b/docs/migration/2211_0/2211-styling.md similarity index 100% rename from docs/migration/2211/2211-styling.md rename to docs/migration/2211_0/2211-styling.md diff --git a/docs/migration/2211/2211-typescript-manual.doc.md b/docs/migration/2211_0/2211-typescript-manual.doc.md similarity index 100% rename from docs/migration/2211/2211-typescript-manual.doc.md rename to docs/migration/2211_0/2211-typescript-manual.doc.md diff --git a/docs/migration/2211_0/migration-comments-api-elements.json b/docs/migration/2211_0/migration-comments-api-elements.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/docs/migration/2211_0/migration-comments-api-elements.json @@ -0,0 +1 @@ +[] diff --git a/docs/migration/2211_0/migration-comments-members.json b/docs/migration/2211_0/migration-comments-members.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/docs/migration/2211_0/migration-comments-members.json @@ -0,0 +1 @@ +[] diff --git a/docs/migration/2211_0/renamed-api-mappings.json b/docs/migration/2211_0/renamed-api-mappings.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/docs/migration/2211_0/renamed-api-mappings.json @@ -0,0 +1 @@ +[] diff --git a/feature-libs/user/_index.scss b/feature-libs/user/_index.scss index 5a613d81a52..e86984fedb2 100644 --- a/feature-libs/user/_index.scss +++ b/feature-libs/user/_index.scss @@ -7,7 +7,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-close-account-modal !default; + cx-close-account-modal, cx-my-account-v2-profile, cx-my-account-v2-email, + cx-my-account-v2-password !default; @each $selector in $selectors { #{$selector} { diff --git a/feature-libs/user/profile/assets/translations/en/index.ts b/feature-libs/user/profile/assets/translations/en/index.ts index b38f5f5780c..540d3d9bccc 100644 --- a/feature-libs/user/profile/assets/translations/en/index.ts +++ b/feature-libs/user/profile/assets/translations/en/index.ts @@ -4,10 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { myAccountV2UserProfile } from './my-account-v2-user-profile.18n'; +import { myAccountV2Email } from './my-account-v2-email.18n'; +import { myAccountV2Password } from './my-account-v2-password.i18n'; import userProfile from './userProfile.json'; import address from './address.json'; export const en = { userProfile, + myAccountV2UserProfile, + myAccountV2Email, + myAccountV2Password, address, }; diff --git a/feature-libs/user/profile/assets/translations/en/my-account-v2-email.18n.ts b/feature-libs/user/profile/assets/translations/en/my-account-v2-email.18n.ts new file mode 100644 index 00000000000..87734806f5d --- /dev/null +++ b/feature-libs/user/profile/assets/translations/en/my-account-v2-email.18n.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const myAccountV2Email = { + myAccountV2Email: { + myEmailAddress: 'My Email Address', + email: 'Email', + emailAddress: 'Email Address', + newEmailAddress: 'New Email Address', + confirmNewEmailAddress: 'Confirm New Email Address', + emailPlaceHolder: 'Enter email', + passwordPlaceHolder: 'Enter password', + password: 'Password', + reloginIndicator: + 'You need to log in again after setting a new email address.', + }, +}; diff --git a/feature-libs/user/profile/assets/translations/en/my-account-v2-password.i18n.ts b/feature-libs/user/profile/assets/translations/en/my-account-v2-password.i18n.ts new file mode 100644 index 00000000000..0e892780d0e --- /dev/null +++ b/feature-libs/user/profile/assets/translations/en/my-account-v2-password.i18n.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const myAccountV2Password = { + myAccountV2PasswordForm: { + oldPassword: { + label: 'Old Password', + placeholder: 'Enter Old Password', + }, + oldPasswordIsRequired: 'Old password is required.', + newPassword: { + label: 'New Password', + placeholder: 'Enter New Password', + }, + passwordMinRequirements: + 'Password must be six characters minimum, with one uppercase letter, one number, one symbol', + confirmPassword: { + label: 'Confirm New Password', + placeholder: 'Confirm New Password', + }, + reloginIndicator: 'You need to log in again after setting password.', + bothPasswordMustMatch: 'Both password must match', + passwordUpdateSuccess: 'Password updated with success', + accessDeniedError: 'Access is denied', + newPasswordTitle: 'My Password', + }, +}; diff --git a/feature-libs/user/profile/assets/translations/en/my-account-v2-user-profile.18n.ts b/feature-libs/user/profile/assets/translations/en/my-account-v2-user-profile.18n.ts new file mode 100644 index 00000000000..253222cb43c --- /dev/null +++ b/feature-libs/user/profile/assets/translations/en/my-account-v2-user-profile.18n.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const myAccountV2UserProfile = { + myAccountV2UserProfile: { + myEmailAddress: 'My Email Address', + myInformation: 'My Information', + name: 'Name', + customerId: 'CustomerId', + title: 'Title', + firstName: 'First name', + lastName: 'Last name', + }, +}; diff --git a/feature-libs/user/profile/assets/translations/translations.ts b/feature-libs/user/profile/assets/translations/translations.ts index f5666723f10..8b471bad222 100644 --- a/feature-libs/user/profile/assets/translations/translations.ts +++ b/feature-libs/user/profile/assets/translations/translations.ts @@ -13,6 +13,9 @@ export const userProfileTranslations: TranslationResources = { export const userProfileTranslationChunksConfig: TranslationChunksConfig = { userProfile: ['updateEmailForm', 'register', 'forgottenPassword'], + myAccountV2UserProfile: ['myAccountV2UserProfile'], + myAccountV2Email: ['myAccountV2Email'], + myAccountV2Password: ['myAccountV2PasswordForm'], address: [ 'addressForm', 'addressBook', diff --git a/feature-libs/user/profile/components/update-email/index.ts b/feature-libs/user/profile/components/update-email/index.ts index 1e5d47b3615..37c5d131d0e 100644 --- a/feature-libs/user/profile/components/update-email/index.ts +++ b/feature-libs/user/profile/components/update-email/index.ts @@ -7,3 +7,5 @@ export * from './update-email-component.service'; export * from './update-email.component'; export * from './update-email.module'; +export * from './use-my-account-v2-email.ts'; +export * from './my-account-v2-email.component'; diff --git a/feature-libs/user/profile/components/update-email/my-account-v2-email.component.html b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.html new file mode 100644 index 00000000000..1a729def6c4 --- /dev/null +++ b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.html @@ -0,0 +1,115 @@ + + +
+
+ + +
+
+
+
+ + +
+
+
+ + +
diff --git a/feature-libs/user/profile/components/update-email/my-account-v2-email.component.spec.ts b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.spec.ts new file mode 100644 index 00000000000..11032f22de8 --- /dev/null +++ b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.spec.ts @@ -0,0 +1,190 @@ +import { + ChangeDetectionStrategy, + Component, + DebugElement, +} from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + UntypedFormControl, + UntypedFormGroup, + ReactiveFormsModule, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { I18nTestingModule, User } from '@spartacus/core'; +import { + FormErrorsModule, + PasswordVisibilityToggleModule, +} from '@spartacus/storefront'; +import { UrlTestingModule } from 'projects/core/src/routing/configurable-routes/url-translation/testing/url-testing.module'; +import { BehaviorSubject, Subject, of } from 'rxjs'; +import { MyAccountV2EmailComponent } from './my-account-v2-email.component'; +import createSpy = jasmine.createSpy; +import { UpdateEmailComponentService } from './update-email-component.service'; +import { UserProfileFacade } from '../../root/facade'; + +@Component({ + selector: 'cx-spinner', + template: '', +}) +class MockCxSpinnerComponent {} + +const isBusySubject = new BehaviorSubject(false); +class MockMyAccountV2EmailService + implements Partial +{ + updateSucceed$ = new Subject(); + form: UntypedFormGroup = new UntypedFormGroup({ + oldEmail: new UntypedFormControl(), + email: new UntypedFormControl(), + confirmEmail: new UntypedFormControl(), + password: new UntypedFormControl(), + }); + isUpdating$ = isBusySubject; + save = createSpy().and.stub(); + resetForm = createSpy().and.stub(); +} + +const sampleUser: User = { + uid: 'sampleUid', +}; +class MockNewProfileFacade implements Partial { + get() { + return of(sampleUser); + } +} + +describe('MyAccountV2EmailComponent', () => { + let component: MyAccountV2EmailComponent; + let fixture: ComponentFixture; + let el: DebugElement; + + let service: UpdateEmailComponentService; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + I18nTestingModule, + FormErrorsModule, + RouterTestingModule, + UrlTestingModule, + PasswordVisibilityToggleModule, + ], + declarations: [MyAccountV2EmailComponent, MockCxSpinnerComponent], + providers: [ + { + provide: UpdateEmailComponentService, + useClass: MockMyAccountV2EmailService, + }, + { + provide: UserProfileFacade, + useClass: MockNewProfileFacade, + }, + ], + }) + .overrideComponent(MyAccountV2EmailComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MyAccountV2EmailComponent); + component = fixture.componentInstance; + component.onEdit(); + el = fixture.debugElement; + service = TestBed.inject(UpdateEmailComponentService); + TestBed.inject(UserProfileFacade); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('busy', () => { + it('should disable the submit button when form is disabled', () => { + component.form.disable(); + component.onEdit(); + fixture.detectChanges(); + const submitBtn: HTMLButtonElement = el.query( + By.css('.btn-primary') + ).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 - editing', () => { + it('should enable the submit button', () => { + component.form.enable(); + component.onEdit(); + fixture.detectChanges(); + const submitBtn = el.query(By.css('.btn-primary')); + expect(submitBtn.nativeElement.disabled).toBeFalsy(); + }); + + it('should not show the spinner', () => { + isBusySubject.next(false); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeNull(); + }); + + it('should show cx message strip', () => { + component.onEdit(); + fixture.detectChanges(); + const cxMsg = el.query(By.css('cx-message')); + expect(cxMsg.nativeElement).toBeTruthy(); + }); + + it('should hide cx message strip when close clicked', () => { + component.onEdit(); + component.closeDialogConfirmationAlert(); + fixture.detectChanges(); + const cxMsg = el.query(By.css('cx-message')); + expect(cxMsg).toBeNull(); + }); + }); + + describe('idle - display', () => { + it('should hide the submit button', () => { + component.ngOnInit(); + fixture.detectChanges(); + expect(el.query(By.css('form'))).toBeNull(); + }); + }); + + describe('Form Interactions', () => { + it('should call onSubmit() method on submit', () => { + component.onEdit(); + fixture.detectChanges(); + const request = spyOn(component, 'onSubmit'); + const form = el.query(By.css('form')); + form.triggerEventHandler('submit', null); + expect(request).toHaveBeenCalled(); + }); + + it('should call the service method on submit', () => { + component.form.enable(); + component.onEdit(); + component.onSubmit(); + expect(service.save).toHaveBeenCalled(); + }); + + it('when cancel is called. submit button is not visible', () => { + component.form.enable(); + component.cancelEdit(); + fixture.detectChanges(); + const submitBtn = el.query(By.css('button.btn-primary')); + expect(submitBtn).toBeNull(); + }); + }); +}); diff --git a/feature-libs/user/profile/components/update-email/my-account-v2-email.component.ts b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.ts new file mode 100644 index 00000000000..a25f9525980 --- /dev/null +++ b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + Component, + OnInit, + inject, +} from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { GlobalMessageType, User } from '@spartacus/core'; + +import { UserProfileFacade } from '@spartacus/user/profile/root'; +import { filter } from 'rxjs/operators'; +import { UpdateEmailComponentService } from './update-email-component.service'; + +@Component({ + selector: 'cx-my-account-v2-email', + templateUrl: './my-account-v2-email.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyAccountV2EmailComponent implements OnInit { + protected emailComponentService = inject(UpdateEmailComponentService); + protected userProfile = inject(UserProfileFacade); + form: UntypedFormGroup = this.emailComponentService.form; + isUpdating$: Observable = this.emailComponentService.isUpdating$; + isEditing: boolean; + showingAlert: boolean; + + user$ = this.userProfile + .get() + .pipe(filter((user): user is User => Boolean(user))); + globalMessageType = GlobalMessageType; + + ngOnInit(): void { + this.isEditing = false; + } + onSubmit(): void { + this.emailComponentService.save(); + this.emailComponentService.updateSucceed$.subscribe((res) => { + this.isEditing = !res; + }); + } + + onEdit(): void { + this.isEditing = true; + this.showingAlert = true; + this.form.reset(); + } + + cancelEdit(): void { + this.isEditing = false; + } + + closeDialogConfirmationAlert() { + this.showingAlert = false; + } +} diff --git a/feature-libs/user/profile/components/update-email/update-email-component.service.ts b/feature-libs/user/profile/components/update-email/update-email-component.service.ts index eefe282d31c..f737ecaa0f3 100644 --- a/feature-libs/user/profile/components/update-email/update-email-component.service.ts +++ b/feature-libs/user/profile/components/update-email/update-email-component.service.ts @@ -1,4 +1,5 @@ /* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team * SPDX-FileCopyrightText: 2024 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 @@ -19,7 +20,7 @@ import { } from '@spartacus/core'; import { CustomFormValidators } from '@spartacus/storefront'; import { UserEmailFacade } from '@spartacus/user/profile/root'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() @@ -34,6 +35,8 @@ export class UpdateEmailComponentService { protected busy$ = new BehaviorSubject(false); + updateSucceed$ = new Subject(); + isUpdating$ = this.busy$.pipe( tap((state) => (state === true ? this.form.disable() : this.form.enable())) ); @@ -82,6 +85,8 @@ export class UpdateEmailComponentService { ); this.busy$.next(false); this.form.reset(); + this.updateSucceed$.next(true); + // sets the redirect url after login this.authRedirectService.setRedirectUrl( this.routingService.getUrl({ cxRoute: 'home' }) @@ -101,5 +106,6 @@ export class UpdateEmailComponentService { protected onError(_error: Error): void { this.busy$.next(false); + this.updateSucceed$.next(false); } } diff --git a/feature-libs/user/profile/components/update-email/update-email.module.ts b/feature-libs/user/profile/components/update-email/update-email.module.ts index 0fde6343eae..7521e199bd8 100644 --- a/feature-libs/user/profile/components/update-email/update-email.module.ts +++ b/feature-libs/user/profile/components/update-email/update-email.module.ts @@ -5,7 +5,7 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { NgModule, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { @@ -16,6 +16,7 @@ import { GlobalMessageService, I18nModule, provideDefaultConfig, + provideDefaultConfigFactory, RoutingService, UrlModule, } from '@spartacus/core'; @@ -23,11 +24,23 @@ import { FormErrorsModule, SpinnerModule, PasswordVisibilityToggleModule, + MessageComponentModule, } from '@spartacus/storefront'; import { UserEmailFacade } from '@spartacus/user/profile/root'; import { UpdateEmailComponentService } from './update-email-component.service'; import { UpdateEmailComponent } from './update-email.component'; +import { USE_MY_ACCOUNT_V2_EMAIL } from './use-my-account-v2-email.ts'; +import { MyAccountV2EmailComponent } from './my-account-v2-email.component'; + +const myAccountV2EmailMapping: CmsConfig = { + cmsComponents: { + UpdateEmailComponent: { + component: MyAccountV2EmailComponent, + }, + }, +}; + @NgModule({ imports: [ CommonModule, @@ -39,7 +52,10 @@ import { UpdateEmailComponent } from './update-email.component'; I18nModule, FormErrorsModule, PasswordVisibilityToggleModule, + MessageComponentModule, ], + declarations: [UpdateEmailComponent, MyAccountV2EmailComponent], + exports: [UpdateEmailComponent, MyAccountV2EmailComponent], providers: [ provideDefaultConfig({ cmsComponents: { @@ -62,7 +78,9 @@ import { UpdateEmailComponent } from './update-email.component'; }, }, }), + provideDefaultConfigFactory(() => + inject(USE_MY_ACCOUNT_V2_EMAIL) ? myAccountV2EmailMapping : {} + ), ], - declarations: [UpdateEmailComponent], }) export class UpdateEmailModule {} diff --git a/feature-libs/user/profile/components/update-email/use-my-account-v2-email.ts.ts b/feature-libs/user/profile/components/update-email/use-my-account-v2-email.ts.ts new file mode 100644 index 00000000000..0750ed5998e --- /dev/null +++ b/feature-libs/user/profile/components/update-email/use-my-account-v2-email.ts.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InjectionToken } from '@angular/core'; + +export const USE_MY_ACCOUNT_V2_EMAIL = new InjectionToken( + 'feature flag to enable enhanced UI for email related pages under My-Account', + { providedIn: 'root', factory: () => false } +); diff --git a/feature-libs/user/profile/components/update-password/index.ts b/feature-libs/user/profile/components/update-password/index.ts index 885c34a07bb..86d5dfed811 100644 --- a/feature-libs/user/profile/components/update-password/index.ts +++ b/feature-libs/user/profile/components/update-password/index.ts @@ -7,3 +7,5 @@ export * from './update-password-component.service'; export * from './update-password.component'; export * from './update-password.module'; +export * from './use-my-account-v2-password'; +export * from './my-account-v2-password.component'; diff --git a/feature-libs/user/profile/components/update-password/my-account-v2-password.component.html b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.html new file mode 100644 index 00000000000..c7a2c407244 --- /dev/null +++ b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.html @@ -0,0 +1,104 @@ + +
+ +
+
+ +
+ +
+
+ + + + + + +
+
+ + +
+
+
+
diff --git a/feature-libs/user/profile/components/update-password/my-account-v2-password.component.spec.ts b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.spec.ts new file mode 100644 index 00000000000..95d9309a350 --- /dev/null +++ b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.spec.ts @@ -0,0 +1,151 @@ +import { + ChangeDetectionStrategy, + Component, + DebugElement, +} from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { I18nTestingModule } from '@spartacus/core'; +import { + FormErrorsModule, + PasswordVisibilityToggleModule, +} from '@spartacus/storefront'; +import { UrlTestingModule } from 'projects/core/src/routing/configurable-routes/url-translation/testing/url-testing.module'; +import { BehaviorSubject } from 'rxjs'; +import { MyAccountV2PasswordComponent } from './my-account-v2-password.component'; +import createSpy = jasmine.createSpy; +import { UpdatePasswordComponentService } from './update-password-component.service'; + +@Component({ + selector: 'cx-spinner', + template: '', +}) +class MockCxSpinnerComponent {} + +const isBusySubject = new BehaviorSubject(false); +class MockUpdatePasswordService + implements Partial +{ + form: UntypedFormGroup = new UntypedFormGroup({ + oldPassword: new UntypedFormControl(), + newPassword: new UntypedFormControl(), + newPasswordConfirm: new UntypedFormControl(), + }); + isUpdating$ = isBusySubject; + updatePassword = createSpy().and.stub(); + resetForm = createSpy().and.stub(); +} + +describe('MyAccountV2PasswordComponent', () => { + let component: MyAccountV2PasswordComponent; + let fixture: ComponentFixture; + let el: DebugElement; + + let service: UpdatePasswordComponentService; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + I18nTestingModule, + FormErrorsModule, + RouterTestingModule, + UrlTestingModule, + PasswordVisibilityToggleModule, + ], + declarations: [MyAccountV2PasswordComponent, MockCxSpinnerComponent], + providers: [ + { + provide: UpdatePasswordComponentService, + useClass: MockUpdatePasswordService, + }, + ], + }) + .overrideComponent(MyAccountV2PasswordComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MyAccountV2PasswordComponent); + component = fixture.componentInstance; + el = fixture.debugElement; + service = TestBed.inject(UpdatePasswordComponentService); + fixture.detectChanges(); + }); + + it('should create', () => { + 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.btn-primary') + ).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.btn-primary')); + expect(submitBtn.nativeElement.disabled).toBeFalsy(); + }); + + it('should not show the spinner', () => { + isBusySubject.next(false); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeNull(); + }); + }); + + describe('Form Interactions', () => { + it('should call onSubmit() method on submit', () => { + const request = spyOn(component, 'onSubmit'); + const form = el.query(By.css('form')); + form.triggerEventHandler('submit', null); + expect(request).toHaveBeenCalled(); + }); + + it('should call the service method on submit', () => { + component.onSubmit(); + expect(service.updatePassword).toHaveBeenCalled(); + }); + + it('should clean input box', () => { + fixture.detectChanges(); + const buttons = fixture.debugElement.queryAll( + By.css('.myaccount-password-button-cancel') + ); + buttons[0].triggerEventHandler('click', null); + expect(el.queryAll(By.css('form-control')).length).toEqual(0); + }); + + it('should hide cx message strip when close clicked', () => { + component.closeDialogConfirmationAlert(); + fixture.detectChanges(); + const cxMsg = el.query(By.css('cx-message')); + expect(cxMsg).toBeNull(); + }); + }); +}); diff --git a/feature-libs/user/profile/components/update-password/my-account-v2-password.component.ts b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.ts new file mode 100644 index 00000000000..60e5acbbbf2 --- /dev/null +++ b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { GlobalMessageType } from '@spartacus/core'; +import { Observable } from 'rxjs'; +import { UpdatePasswordComponentService } from './update-password-component.service'; + +@Component({ + selector: 'cx-my-account-v2-password', + templateUrl: './my-account-v2-password.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyAccountV2PasswordComponent { + protected service = inject(UpdatePasswordComponentService); + showingAlert: boolean = true; + globalMessageType = GlobalMessageType; + oldPassword: string; + newPassword: string; + newPasswordConfirm: string; + + form: UntypedFormGroup = this.service.form; + isUpdating$: Observable = this.service.isUpdating$; + + onSubmit(): void { + this.service.updatePassword(); + } + + onCancel(): void { + this.oldPassword = ''; + this.newPassword = ''; + this.newPasswordConfirm = ''; + } + closeDialogConfirmationAlert() { + this.showingAlert = false; + } +} diff --git a/feature-libs/user/profile/components/update-password/update-password-component.service.ts b/feature-libs/user/profile/components/update-password/update-password-component.service.ts index bfacf96542f..6c5d36b5042 100644 --- a/feature-libs/user/profile/components/update-password/update-password-component.service.ts +++ b/feature-libs/user/profile/components/update-password/update-password-component.service.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { UntypedFormControl, UntypedFormGroup, @@ -22,6 +22,7 @@ import { CustomFormValidators } from '@spartacus/storefront'; import { UserPasswordFacade } from '@spartacus/user/profile/root'; import { BehaviorSubject } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { USE_MY_ACCOUNT_V2_PASSWORD } from './use-my-account-v2-password'; @Injectable() export class UpdatePasswordComponentService { @@ -35,6 +36,8 @@ export class UpdatePasswordComponentService { protected busy$ = new BehaviorSubject(false); + private usingV2 = inject(USE_MY_ACCOUNT_V2_PASSWORD); + isUpdating$ = this.busy$.pipe( tap((state) => (state === true ? this.form.disable() : this.form.enable())) ); @@ -42,10 +45,7 @@ export class UpdatePasswordComponentService { form: UntypedFormGroup = new UntypedFormGroup( { oldPassword: new UntypedFormControl('', Validators.required), - newPassword: new UntypedFormControl('', [ - Validators.required, - CustomFormValidators.passwordValidator, - ]), + newPassword: new UntypedFormControl('', Validators.required), newPasswordConfirm: new UntypedFormControl('', Validators.required), }, { @@ -78,7 +78,11 @@ export class UpdatePasswordComponentService { protected onSuccess(): void { this.globalMessageService.add( - { key: 'updatePasswordForm.passwordUpdateSuccess' }, + { + key: this.usingV2 + ? 'myAccountV2PasswordForm.passwordUpdateSuccess' + : 'updatePasswordForm.passwordUpdateSuccess', + }, GlobalMessageType.MSG_TYPE_CONFIRMATION ); this.busy$.next(false); @@ -100,7 +104,11 @@ export class UpdatePasswordComponentService { _error.details?.[0].type === 'AccessDeniedError' ) { this.globalMessageService.add( - { key: 'updatePasswordForm.accessDeniedError' }, + { + key: this.usingV2 + ? 'myAccountV2PasswordForm.accessDeniedError' + : 'updatePasswordForm.accessDeniedError', + }, GlobalMessageType.MSG_TYPE_ERROR ); } diff --git a/feature-libs/user/profile/components/update-password/update-password.module.ts b/feature-libs/user/profile/components/update-password/update-password.module.ts index c9d958adc0f..ffd1cb01be0 100644 --- a/feature-libs/user/profile/components/update-password/update-password.module.ts +++ b/feature-libs/user/profile/components/update-password/update-password.module.ts @@ -5,7 +5,7 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { NgModule, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { @@ -16,11 +16,13 @@ import { GlobalMessageService, I18nModule, provideDefaultConfig, + provideDefaultConfigFactory, RoutingService, UrlModule, } from '@spartacus/core'; import { FormErrorsModule, + MessageComponentModule, PasswordVisibilityToggleModule, SpinnerModule, } from '@spartacus/storefront'; @@ -28,6 +30,17 @@ import { UserPasswordFacade } from '@spartacus/user/profile/root'; import { UpdatePasswordComponentService } from './update-password-component.service'; import { UpdatePasswordComponent } from './update-password.component'; +import { USE_MY_ACCOUNT_V2_PASSWORD } from './use-my-account-v2-password'; +import { MyAccountV2PasswordComponent } from './my-account-v2-password.component'; + +const myAccountV2PasswordMapping: CmsConfig = { + cmsComponents: { + UpdatePasswordComponent: { + component: MyAccountV2PasswordComponent, + }, + }, +}; + @NgModule({ imports: [ CommonModule, @@ -39,6 +52,7 @@ import { UpdatePasswordComponent } from './update-password.component'; UrlModule, RouterModule, PasswordVisibilityToggleModule, + MessageComponentModule, ], providers: [ provideDefaultConfig({ @@ -62,7 +76,11 @@ import { UpdatePasswordComponent } from './update-password.component'; }, }, }), + provideDefaultConfigFactory(() => + inject(USE_MY_ACCOUNT_V2_PASSWORD) ? myAccountV2PasswordMapping : {} + ), ], - declarations: [UpdatePasswordComponent], + declarations: [UpdatePasswordComponent, MyAccountV2PasswordComponent], + exports: [UpdatePasswordComponent, MyAccountV2PasswordComponent], }) export class UpdatePasswordModule {} diff --git a/feature-libs/user/profile/components/update-password/use-my-account-v2-password.ts b/feature-libs/user/profile/components/update-password/use-my-account-v2-password.ts new file mode 100644 index 00000000000..cbf0ed0c84b --- /dev/null +++ b/feature-libs/user/profile/components/update-password/use-my-account-v2-password.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InjectionToken } from '@angular/core'; + +export const USE_MY_ACCOUNT_V2_PASSWORD = new InjectionToken( + 'feature flag to enable enhanced UI for Update Password related pages under My-Account', + { providedIn: 'root', factory: () => false } +); diff --git a/feature-libs/user/profile/components/update-profile/index.ts b/feature-libs/user/profile/components/update-profile/index.ts index d36812d95f4..1795d62cf6a 100644 --- a/feature-libs/user/profile/components/update-profile/index.ts +++ b/feature-libs/user/profile/components/update-profile/index.ts @@ -7,3 +7,5 @@ export * from './update-profile-component.service'; export * from './update-profile.component'; export * from './update-profile.module'; +export * from './use-my-account-v2-profile'; +export * from './my-account-v2-profile.component'; diff --git a/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.html b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.html new file mode 100644 index 00000000000..9f2e548b288 --- /dev/null +++ b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.html @@ -0,0 +1,120 @@ + + +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + + + +
+
+
+ + +
+
+
+
diff --git a/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.spec.ts b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.spec.ts new file mode 100644 index 00000000000..0ccac7a0900 --- /dev/null +++ b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.spec.ts @@ -0,0 +1,159 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + DebugElement, +} from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + UntypedFormControl, + UntypedFormGroup, + ReactiveFormsModule, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { FeaturesConfigModule, I18nTestingModule } from '@spartacus/core'; +import { FormErrorsModule } from '@spartacus/storefront'; +import { UrlTestingModule } from 'projects/core/src/routing/configurable-routes/url-translation/testing/url-testing.module'; +import { BehaviorSubject, Subject, of } from 'rxjs'; +import { MyAccountV2ProfileComponent } from './my-account-v2-profile.component'; +import createSpy = jasmine.createSpy; +import { UpdateProfileComponentService } from './update-profile-component.service'; +@Component({ + selector: 'cx-spinner', + template: `
spinner
`, +}) +class MockCxSpinnerComponent {} + +const isBusySubject = new BehaviorSubject(false); + +class MockProfileService implements Partial { + user$ = of({}); + titles$ = of([]); + updateSucceed$ = new Subject(); + form: UntypedFormGroup = new UntypedFormGroup({ + customerId: new UntypedFormControl(), + titleCode: new UntypedFormControl(), + firstName: new UntypedFormControl(), + lastName: new UntypedFormControl(), + }); + isUpdating$ = isBusySubject; + updateProfile = createSpy().and.stub(); +} + +describe('MyAccountV2ProfileComponent', () => { + let component: MyAccountV2ProfileComponent; + let fixture: ComponentFixture; + let el: DebugElement; + + let service: UpdateProfileComponentService; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + I18nTestingModule, + FormErrorsModule, + RouterTestingModule, + UrlTestingModule, + NgSelectModule, + FeaturesConfigModule, + ], + declarations: [MyAccountV2ProfileComponent, MockCxSpinnerComponent], + providers: [ + { + provide: UpdateProfileComponentService, + useClass: MockProfileService, + }, + ], + }) + .overrideComponent(MyAccountV2ProfileComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MyAccountV2ProfileComponent); + component = fixture.componentInstance; + el = fixture.debugElement; + component.onEdit(); + service = TestBed.inject(UpdateProfileComponentService); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('busy', () => { + it('should disable the submit button when form is disabled', () => { + component.form.disable(); + component.onEdit(); + fixture.detectChanges(); + const submitBtn: HTMLButtonElement = el.query( + By.css('.btn-primary') + ).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 - editing', () => { + it('should enable the submit button', () => { + component.form.enable(); + component.onEdit(); + fixture.detectChanges(); + const submitBtn = el.query(By.css('.btn-primary')); + 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('idle - display', () => { + it('should hide the submit button', () => { + component.ngOnInit(); + fixture.detectChanges(); + expect(el.query(By.css('form'))).toBeNull(); + }); + }); + + describe('Form Interactions', () => { + it('should call onSubmit() method on submit', () => { + component.onEdit(); + fixture.detectChanges(); + const request = spyOn(component, 'onSubmit'); + const form = el.query(By.css('form')); + form.triggerEventHandler('submit', null); + expect(request).toHaveBeenCalled(); + }); + + it('should call the service method on submit', () => { + component.onSubmit(); + expect(service.updateProfile).toHaveBeenCalled(); + }); + + it('when cancel is called. submit button is not visible', () => { + component.form.enable(); + fixture.detectChanges(); + component.cancelEdit(); + const submitBtn = el.query(By.css('button.btn-primary')); + expect(submitBtn).toBeNull(); + }); + }); +}); diff --git a/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.ts b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.ts new file mode 100644 index 00000000000..a2e3005e794 --- /dev/null +++ b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + Component, + OnInit, + inject, +} from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { Title } from '@spartacus/user/profile/root'; +import { User } from '@spartacus/user/account/root'; +import { Observable } from 'rxjs'; +import { UpdateProfileComponentService } from './update-profile-component.service'; + +@Component({ + selector: 'cx-my-account-v2-profile', + templateUrl: './my-account-v2-profile.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyAccountV2ProfileComponent implements OnInit { + protected service = inject(UpdateProfileComponentService); + ngOnInit(): void { + this.isEditing = false; + } + + form: UntypedFormGroup = this.service.form; + isUpdating$ = this.service.isUpdating$; + titles$: Observable = this.service.titles$; + user$: Observable = this.service.user$; + isEditing: boolean; + originalEditValue: User; + + onSubmit(): void { + this.service.updateProfile(); + this.service.updateSucceed$.subscribe((res) => { + this.isEditing = !res; + }); + } + + cancelEdit(): void { + this.isEditing = false; + this.form.setValue(this.originalEditValue); + } + + onEdit(): void { + this.isEditing = true; + this.originalEditValue = this.form.value; + } +} diff --git a/feature-libs/user/profile/components/update-profile/update-profile-component.service.ts b/feature-libs/user/profile/components/update-profile/update-profile-component.service.ts index fc43cfb3d4a..39cea0418df 100644 --- a/feature-libs/user/profile/components/update-profile/update-profile-component.service.ts +++ b/feature-libs/user/profile/components/update-profile/update-profile-component.service.ts @@ -1,4 +1,5 @@ /* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team * SPDX-FileCopyrightText: 2024 SAP Spartacus team * * SPDX-License-Identifier: Apache-2.0 @@ -13,7 +14,7 @@ import { import { GlobalMessageService, GlobalMessageType } from '@spartacus/core'; import { User } from '@spartacus/user/account/root'; import { Title, UserProfileFacade } from '@spartacus/user/profile/root'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter, switchMap, tap } from 'rxjs/operators'; @Injectable() @@ -23,12 +24,14 @@ export class UpdateProfileComponentService { protected globalMessageService: GlobalMessageService ) {} - protected user$ = this.userProfile + user$ = this.userProfile .get() .pipe(filter((user): user is User => Boolean(user))); protected busy$ = new BehaviorSubject(false); + updateSucceed$ = new Subject(); + isUpdating$: Observable = this.user$.pipe( tap((user) => this.form.patchValue(user)), switchMap((_user: User) => this.busy$), @@ -71,9 +74,11 @@ export class UpdateProfileComponentService { this.busy$.next(false); this.form.reset(); + this.updateSucceed$.next(true); } protected onError(_error: Error): void { this.busy$.next(false); + this.updateSucceed$.next(false); } } diff --git a/feature-libs/user/profile/components/update-profile/update-profile.module.ts b/feature-libs/user/profile/components/update-profile/update-profile.module.ts index 78cf42bdc2d..2e7b571447f 100644 --- a/feature-libs/user/profile/components/update-profile/update-profile.module.ts +++ b/feature-libs/user/profile/components/update-profile/update-profile.module.ts @@ -5,7 +5,7 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { NgModule, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -15,6 +15,7 @@ import { GlobalMessageService, I18nModule, provideDefaultConfig, + provideDefaultConfigFactory, UrlModule, } from '@spartacus/core'; import { @@ -23,8 +24,18 @@ import { NgSelectA11yModule, } from '@spartacus/storefront'; import { UserProfileFacade } from '@spartacus/user/profile/root'; -import { UpdateProfileComponentService } from './update-profile-component.service'; import { UpdateProfileComponent } from './update-profile.component'; +import { USE_MY_ACCOUNT_V2_PROFILE } from './use-my-account-v2-profile'; +import { UpdateProfileComponentService } from './update-profile-component.service'; +import { MyAccountV2ProfileComponent } from './my-account-v2-profile.component'; + +const myAccountV2ProfileMapping: CmsConfig = { + cmsComponents: { + UpdateProfileComponent: { + component: MyAccountV2ProfileComponent, + }, + }, +}; @NgModule({ imports: [ @@ -39,6 +50,8 @@ import { UpdateProfileComponent } from './update-profile.component'; NgSelectModule, NgSelectA11yModule, ], + declarations: [UpdateProfileComponent, MyAccountV2ProfileComponent], + exports: [UpdateProfileComponent, MyAccountV2ProfileComponent], providers: [ provideDefaultConfig({ cmsComponents: { @@ -55,7 +68,9 @@ import { UpdateProfileComponent } from './update-profile.component'; }, }, }), + provideDefaultConfigFactory(() => + inject(USE_MY_ACCOUNT_V2_PROFILE) ? myAccountV2ProfileMapping : {} + ), ], - declarations: [UpdateProfileComponent], }) export class UpdateProfileModule {} diff --git a/feature-libs/user/profile/components/update-profile/use-my-account-v2-profile.ts b/feature-libs/user/profile/components/update-profile/use-my-account-v2-profile.ts new file mode 100644 index 00000000000..69a7a9c6c72 --- /dev/null +++ b/feature-libs/user/profile/components/update-profile/use-my-account-v2-profile.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InjectionToken } from '@angular/core'; + +export const USE_MY_ACCOUNT_V2_PROFILE = new InjectionToken( + 'feature flag to enable enhanced UI for profile related pages under My-Account', + { providedIn: 'root', factory: () => false } +); diff --git a/feature-libs/user/profile/styles/_index.scss b/feature-libs/user/profile/styles/_index.scss index 6c609f38566..be059edd6c7 100644 --- a/feature-libs/user/profile/styles/_index.scss +++ b/feature-libs/user/profile/styles/_index.scss @@ -3,6 +3,9 @@ @import './forget-password'; @import './register'; @import './update-password-form'; +@import './my-account-v2/my-account-v2-email'; +@import './my-account-v2/my-account-v2-profile'; +@import './my-account-v2/my-account-v2-password'; @import './address-book'; @import './address-form'; @import './suggested-addresses-dialog'; diff --git a/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-email.scss b/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-email.scss new file mode 100644 index 00000000000..c6ab9ca39f9 --- /dev/null +++ b/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-email.scss @@ -0,0 +1,84 @@ +%cx-my-account-v2-email { + .flex-line { + display: flex; + flex-direction: row; + align-items: baseline; + } + + .headertext { + @include type('3'); + padding-bottom: 1.5rem; + width: 95%; + } + + .editButton { + @include type(); + color: var(--cx-color-primary); + border: none; + background-color: var(--cx-color-transparent); + } + + .text-head { + @include type('6'); + min-width: 7.5rem; + color: var(--cx-color-background-dark); + width: 15%; + } + + .value { + @include type(); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .button { + @include type(); + padding-top: 0.8rem; + padding-inline-end: 1.5rem; + padding-bottom: 0.8rem; + padding-inline-start: 0.8rem; + } + + .button-cancel { + color: var(--cx-color-primary); + } + + .email-editing-area { + width: 100%; + max-width: 35rem; + } + + .cx-message-info { + @include type('7'); + border-color: var(--cx-color-visual-focus); + background-color: var(--cx-color-info-accent); + .cx-message-icon { + cx-icon { + color: var(--cx-color-primary-accent); + } + } + } + + .btn-group-row { + padding-top: 0.75rem; + display: flex; + flex: 100%; + justify-content: end; + } + + .btn-group { + display: flex; + justify-content: space-between; + width: 40%; + } + + form { + label { + padding-bottom: 0.75rem; + } + } + label { + padding-bottom: 0.3rem; + } +} diff --git a/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-password.scss b/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-password.scss new file mode 100644 index 00000000000..348e7394c01 --- /dev/null +++ b/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-password.scss @@ -0,0 +1,59 @@ +%cx-my-account-v2-password { + .myaccount-password-header { + @include type('3'); + padding-bottom: 1.5rem; + } + + .myaccount-password-button { + font-weight: var(--cx-font-weight-normal); + } + + .myaccount-password-button-cancel { + color: var(--cx-color-primary); + } + + .myaccount-password-label { + @include type(); + min-width: 7.5rem; + padding-top: 0; + padding-inline-end: 1.2rem; + padding-bottom: 0; + padding-inline-start: 0; + } + + .myaccount-label-padding { + padding-bottom: 0.75rem; + } + + .password-btn-group-row { + display: flex; + flex: 100%; + justify-content: end; + } + + .password-btn-group { + display: flex; + justify-content: space-between; + width: 40%; + } + + .center-location { + display: flex; + justify-content: center; + } + + .half-width { + width: 50%; + } + + .cx-message-info { + @include type('7'); + border-color: var(--cx-color-visual-focus); + background-color: var(--cx-color-info-accent); + .cx-message-icon { + cx-icon { + color: var(--cx-color-primary-accent); + } + } + } +} diff --git a/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-profile.scss b/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-profile.scss new file mode 100644 index 00000000000..1c292640b02 --- /dev/null +++ b/feature-libs/user/profile/styles/my-account-v2/_my-account-v2-profile.scss @@ -0,0 +1,76 @@ +%cx-my-account-v2-profile { + .flex-line { + display: flex; + flex-direction: row; + align-items: baseline; + } + + .headertext { + @include type('3'); + padding-bottom: 1.5rem; + width: 95%; + } + + .editButton { + @include type(); + color: var(--cx-color-primary); + border: none; + background-color: var(--cx-color-transparent); + } + + .text-head { + @include type('6'); + min-width: 7.5rem; + width: 15%; + } + + .value { + @include type('7'); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .button { + @include type(); + padding-top: 0.8rem; + padding-inline-end: 1.5rem; + padding-bottom: 0.8rem; + padding-inline-start: 1.5rem; + } + + .button-cancel { + color: var(--cx-color-primary); + } + + .myaccount-editing-area { + width: 100%; + max-width: 35rem; + } + + .btn-group-row { + padding-top: 0.75rem; + display: flex; + flex: 100%; + justify-content: end; + } + + .btn-group { + display: flex; + justify-content: space-between; + width: 40%; + } + + .ng-select .ng-select-container { + background-color: var(--cx-color-background); + } + + form { + label { + padding-bottom: 0.75rem; + } + } + label { + padding-bottom: 0.3rem; + } +} diff --git a/projects/assets/src/translations/en/index.ts b/projects/assets/src/translations/en/index.ts index 4e02c3e0a8a..74ed43cf8ec 100644 --- a/projects/assets/src/translations/en/index.ts +++ b/projects/assets/src/translations/en/index.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import myAccountV2NotifiationPerference from './my-account-v2-notification-perference.json'; +import myAccountV2Consent from './my-account-v2-consent.json'; import common from './common.json'; import deliveryMode from './deliveryMode.json'; import myAccount from './myAccount.json'; @@ -24,4 +26,6 @@ export const en = { video, pdf, deliveryMode, + myAccountV2Consent, + myAccountV2NotifiationPerference, }; diff --git a/projects/assets/src/translations/en/my-account-v2-consent.json b/projects/assets/src/translations/en/my-account-v2-consent.json new file mode 100644 index 00000000000..27533c8cb20 --- /dev/null +++ b/projects/assets/src/translations/en/my-account-v2-consent.json @@ -0,0 +1,14 @@ +{ + "myAccountV2Consent": { + "header": "Consent Management", + "dateDescription": "Approved on ", + "clearAll": "Disable All", + "selectAll": "Enable All", + "message": { + "success": { + "given": "Consent successfully given.", + "withdrawn": "Consent successfully withdrawn." + } + } + } +} diff --git a/projects/assets/src/translations/en/my-account-v2-notification-perference.json b/projects/assets/src/translations/en/my-account-v2-notification-perference.json new file mode 100644 index 00000000000..cd29c7ead28 --- /dev/null +++ b/projects/assets/src/translations/en/my-account-v2-notification-perference.json @@ -0,0 +1,9 @@ +{ + "myAccountV2NotifiationPerference": { + "header": "Notification Channels", + "message": "Select your preferred notification channels", + "note": "Note: ", + "noteMessage": "If you deactivate all channels you will not be able to receive any further notifications.", + "EMAIL": "Email:" + } +} diff --git a/projects/assets/src/translations/translation-chunks-config.ts b/projects/assets/src/translations/translation-chunks-config.ts index 90c270a27cb..e8b49a68395 100644 --- a/projects/assets/src/translations/translation-chunks-config.ts +++ b/projects/assets/src/translations/translation-chunks-config.ts @@ -67,4 +67,6 @@ export const translationChunksConfig: TranslationChunksConfig = { user: ['anonymousConsents', 'loginRegister', 'checkoutLogin', 'authMessages'], video: ['player'], deliveryMode: ['setDeliveryMode'], + myAccountV2NotifiationPerference: ['myAccountV2NotifiationPerference'], + myAccountV2Consent: ['myAccountV2Consent'], }; diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/tabbing-order.e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/tabbing-order.e2e-spec-flaky.cy.ts index 81964e153aa..1f07f15160b 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/tabbing-order.e2e-spec-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/tabbing-order.e2e-spec-flaky.cy.ts @@ -36,9 +36,12 @@ import { import { changePasswordTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/change-password'; import { closeAccountTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/close-account'; import { consentManagementTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/consent-management'; +import { myAccountV2consentManagementTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/my-account-v2-consent-management'; +import { myAccountV2PasswordTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/my-account-v2-password'; import { checkoutMyCouponsTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/my-coupons'; import { myInterestTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/my-interests'; import { notificationPreferenceTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/notification-preference'; +import { myAccountV2notificationPreferenceTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/my-account-v2-notification-preference'; import { orderDetailsTabbingOrder } from '../../helpers/accessibility/tabbing-order/my-account/order-details'; import { orderHistoryNoOrdersTabbingOrder, @@ -368,6 +371,14 @@ describe('Tabbing order - tests do require user to be logged in', () => { }); }); + context('My acoount V2 notification preference', () => { + it('should allow to navigate with tab key', () => { + myAccountV2notificationPreferenceTabbingOrder( + config.myAccountV2NotificationPreference + ); + }); + }); + context('Change password', () => { it('should allow to navigate with tab key', () => { changePasswordTabbingOrder(config.changePassword); @@ -398,6 +409,22 @@ describe('Tabbing order - tests do require user to be logged in', () => { }); }); + context('My Account V2 Consent Management(CXSPA-4491)', () => { + it('should allow to navigate with tab key', () => { + cy.requireLoggedIn(); + myAccountV2consentManagementTabbingOrder( + config.myAccountV2ConsentManagement + ); + }); + }); + + context('My Account V2 Password(CXSPA-4455)', () => { + it('should allow to navigate with tab key', () => { + cy.requireLoggedIn(); + myAccountV2PasswordTabbingOrder(config.myAccountV2Password); + }); + }); + context('Address Book (Form)', () => { it('should allow to navigate with tab key (Directory)', () => { setupForAddressBookTests(); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/my-account-v2-email-management.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/my-account-v2-email-management.ts new file mode 100644 index 00000000000..e7809aa06bd --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/my-account-v2-email-management.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { verifyTabbingOrder } from '../../../../helpers/accessibility/tabbing-order'; +import { TabElement } from '../../../../helpers/accessibility/tabbing-order.model'; + +const containerSelector = 'cx-my-new-account-v2-email'; + +export function myAccountV2UserEmailManagementTabbingOrder( + config: TabElement[], + isEdit: boolean = false +) { + cy.intercept({ + method: 'GET', + pathname: `${Cypress.env('OCC_PREFIX')}/${Cypress.env( + 'BASE_SITE' + )}/cms/components`, + }).as('getComponents'); + cy.visit('/my-account/update-email'); + + cy.wait('@getComponents'); + + if (isEdit) { + cy.get('.email-enhancedUI-editButton').click(); + } + + verifyTabbingOrder(containerSelector, config); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/my-account-v2-profile-management.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/my-account-v2-profile-management.ts new file mode 100644 index 00000000000..7543548d2ab --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/my-account-v2-profile-management.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { verifyTabbingOrder } from '../../../../helpers/accessibility/tabbing-order'; +import { TabElement } from '../../../../helpers/accessibility/tabbing-order.model'; + +const containerSelector = 'cx-my-new-account-v2-profile'; + +export function myAccountV2UserProfileManagementTabbingOrder( + config: TabElement[], + isEdit: boolean = false +) { + cy.intercept({ + method: 'GET', + pathname: `${Cypress.env('OCC_PREFIX')}/${Cypress.env( + 'BASE_SITE' + )}/cms/components`, + }).as('getComponents'); + cy.visit('/my-account/update-profile'); + + cy.wait('@getComponents'); + if (isEdit) { + cy.get('.myaccount-enhancedUI-editButton').click(); + } + + verifyTabbingOrder(containerSelector, config); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/tabbing-order.config.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/tabbing-order.config.ts new file mode 100644 index 00000000000..af64c5dfb4d --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/tabbing-order.config.ts @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + TabbingOrderTypes, + TabbingOrderConfig, +} from '../../../../helpers/accessibility/tabbing-order.model'; + +export const tabbingOrderConfig: TabbingOrderConfig = { + myAccountV2EmailDisplay: [ + { + value: 'Edit', + type: TabbingOrderTypes.BUTTON, + }, + ], + myAccountV2EmailEdit: [ + { + type: TabbingOrderTypes.BUTTON, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.BUTTON, + }, + { + value: 'Cancel', + type: TabbingOrderTypes.BUTTON, + }, + { + value: 'Save', + type: TabbingOrderTypes.BUTTON, + }, + ], + myAccountV2ProfileDisplay: [ + { + value: 'Edit', + type: TabbingOrderTypes.BUTTON, + }, + ], + myAccountV2ProfileEdit: [ + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + value: 'Cancel', + type: TabbingOrderTypes.BUTTON, + }, + { + value: 'Save', + type: TabbingOrderTypes.BUTTON, + }, + ], +}; diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/tabbing-order.e2e-my-account-v2.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/tabbing-order.e2e-my-account-v2.cy.ts new file mode 100644 index 00000000000..a19d1baf7d5 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/accessibility/tabbing-order.e2e-my-account-v2.cy.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { myAccountV2UserEmailManagementTabbingOrder } from './my-account-v2-email-management'; +import { myAccountV2UserProfileManagementTabbingOrder } from './my-account-v2-profile-management'; + +import { tabbingOrderConfig as config } from './tabbing-order.config'; + +describe('Tabbing order - tests do require user to be logged in display model', () => { + before(() => { + cy.requireLoggedIn(); + }); + + beforeEach(() => { + cy.requireLoggedIn(); + }); + + afterEach(() => { + cy.restoreLocalStorage(); + }); + + context('My Account V2 Profile Management ', () => { + it('should allow to navigate with tab key display mode (CXSPA-4442)', () => { + myAccountV2UserProfileManagementTabbingOrder( + config.myAccountV2ProfileDisplay + ); + }); + + it('should allow to navigate with tab key edit mode (CXSPA-4442)', () => { + myAccountV2UserProfileManagementTabbingOrder( + config.myAccountV2ProfileEdit, + true + ); + }); + }); + + context('My Account V2 Email Management', () => { + it('should allow to navigate with tab key display mode (CXSPA-4442)', () => { + myAccountV2UserEmailManagementTabbingOrder( + config.myAccountV2EmailDisplay + ); + }); + it('should allow to navigate with tab key edit mode (CXSPA-4442)', () => { + myAccountV2UserEmailManagementTabbingOrder( + config.myAccountV2EmailEdit, + true + ); + }); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-consent-management.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-consent-management.e2e.cy.ts new file mode 100644 index 00000000000..6af9b31fc16 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-consent-management.e2e.cy.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + myAccountV2consentManagementTest, + verifyAsAnonymous, +} from '../../../helpers/consent-management'; +import * as login from '../../../helpers/login'; +import { viewportContext } from '../../../helpers/viewport-context'; +import { isolateTests } from '../../../support/utils/test-isolation'; + +viewportContext(['mobile', 'desktop'], () => { + describe('My Account - Consent Management', () => { + before(() => { + cy.window().then((win) => win.sessionStorage.clear()); + }); + describe('consent management test for anonymous user(CXSPA-4491)', () => { + verifyAsAnonymous(); + }); + + describe( + 'consent management test for logged in user(CXSPA-4491)', + { testIsolation: false }, + () => { + isolateTests(); + before(() => { + cy.requireLoggedIn(); + cy.reload(); + cy.visit('/'); + cy.selectUserMenuOption({ + option: 'Consent Management', + }); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + }); + + myAccountV2consentManagementTest(); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + after(() => { + login.signOutUser(); + }); + } + ); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-email.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-email.e2e.cy.ts new file mode 100644 index 00000000000..818d937c4f7 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-email.e2e.cy.ts @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { login } from '../../../helpers/auth-forms'; +import * as alerts from '../../../helpers/global-message'; +import { signOut } from '../../../helpers/register'; +import * as updateEmail from '../../../helpers/update-email'; +import { registerAndLogin } from '../../../helpers/update-email'; +import { viewportContext } from '../../../helpers/viewport-context'; +import { standardUser } from '../../../sample-data/shared-users'; +import { isolateTests } from '../../../support/utils/test-isolation'; + +describe('My Account - Update Email', () => { + viewportContext(['mobile', 'desktop'], () => { + before(() => { + cy.window().then((win) => win.sessionStorage.clear()); + }); + + describe('Anonymous user', () => { + it('should redirect to login page (CXSPA-4442)', () => { + cy.visit(updateEmail.UPDATE_EMAIL_URL); + cy.location('pathname').should('contain', '/login'); + }); + }); + + describe('Logged in user', { testIsolation: false }, () => { + isolateTests(); + before(() => { + registerAndLogin(); + cy.visit('/'); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.selectUserMenuOption({ + option: 'Email Address', + }); + }); + + it('should click edit email and go to edit menu, and cancel works as expected (CXSPA-4442)', () => { + cy.get('.cx-message-info').should('not.exist'); + cy.get('.email-enhancedUI-value').should('exist'); + + cy.log('--> click edit button'); + cy.get('.email-enhancedUI-editButton').click(); + + cy.log('--> should show email message bar'); + cy.get('.cx-message-info').should('exist'); + cy.get('email-enhancedUI-value').should('not.exist'); + cy.get('.email-enhancedUI-button-cancel').should('exist'); + + cy.log('--> click cancel button'); + cy.get('.email-enhancedUI-button-cancel').click(); + + cy.log('--> should show email content'); + cy.get('.cx-message-info').should('not.exist'); + cy.get('.email-enhancedUI-value').should('exist'); + + cy.log('--> click edit button'); + cy.get('.email-enhancedUI-editButton').click(); + }); + + // Core e2e test. Check with different view port. + updateEmail.testUpdateEmailAndLogin(); + + // Below test depends on core test for setup. + it('should not allow login with old email address (CXSPA-4442)', () => { + signOut(); + cy.visit('/login'); + login( + standardUser.registrationData.email, + standardUser.registrationData.password + ); + alerts.getErrorAlert().should('contain', 'Bad credentials'); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + }); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-notification-preference.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-notification-preference.e2e.cy.ts new file mode 100644 index 00000000000..87138626711 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-notification-preference.e2e.cy.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + testEnableDisableMyAccountV2NotificationPreference, + updateEmail, + verifyEmailChannelV2, +} from '../../../helpers/notification'; +import { registerAndLogin } from '../../../helpers/update-email'; +import { viewportContext } from '../../../helpers/viewport-context'; +import { standardUser } from '../../../sample-data/shared-users'; +import { clearAllStorage } from '../../../support/utils/clear-all-storage'; + +describe('My Account V2 Notification preference', () => { + viewportContext(['mobile'], () => { + describe('Logged in user', () => { + before(() => { + clearAllStorage(); + registerAndLogin(); + cy.visit('/'); + }); + + // Core test. Run in mobile view as well. + testEnableDisableMyAccountV2NotificationPreference(); + }); + }); + + viewportContext(['mobile', 'desktop'], () => { + describe('Anonymous user', () => { + before(() => { + clearAllStorage(); + }); + + it('should redirect to login page for anonymous user', () => { + cy.visit('/my-account/notification-preference'); + cy.location('pathname').should('contain', '/login'); + }); + }); + + describe('Logged in user', () => { + before(() => { + clearAllStorage(); + registerAndLogin(); + cy.visit('/'); + }); + + it('should show correct email channel after update email address', () => { + verifyEmailChannelV2(standardUser.registrationData.email); + const newEmail = updateEmail(); + verifyEmailChannelV2(newEmail); + }); + }); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-password.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-password.e2e.cy.ts new file mode 100644 index 00000000000..b957514e8d7 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-password.e2e.cy.ts @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as alerts from '../../../helpers/global-message'; +import { signOutUser } from '../../../helpers/login'; +import * as updatePassword from '../../../helpers/update-password'; +import { generateMail, randomString } from '../../../helpers/user'; +import { viewportContext } from '../../../helpers/viewport-context'; +import { standardUser } from '../../../sample-data/shared-users'; +import { isolateTests } from '../../../support/utils/test-isolation'; + +describe('My Account V2 - Update Password', () => { + viewportContext(['mobile'], () => { + before(() => + cy.window().then((win) => { + win.sessionStorage.clear(); + }) + ); + // Core e2e test. Repeat in mobile viewport. + updatePassword.testUpdatePasswordLoggedInUser(true); + }); + + viewportContext(['mobile', 'desktop'], () => { + before(() => + cy.window().then((win) => { + win.sessionStorage.clear(); + }) + ); + + describe('update password test for anonymous user', () => { + it('should redirect to login page for anonymous user', () => { + cy.visit(updatePassword.PAGE_URL_UPDATE_PASSWORD); + cy.url().should('contain', '/login'); + }); + }); + + describe( + 'update password test for logged in user', + { testIsolation: false }, + () => { + isolateTests(); + before(() => { + standardUser.registrationData.email = generateMail( + randomString(), + true + ); + cy.requireLoggedIn(standardUser); + cy.visit('/'); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.selectUserMenuOption({ + option: 'Password', + }); + }); + + it('should be able to cancel the input in password columns', () => { + cy.get('[formcontrolname="oldPassword"]').type('wrongpassword'); + cy.get('[formcontrolname="newPassword"]').type( + updatePassword.newPassword + ); + cy.get('[formcontrolname="newPasswordConfirm"]').type( + updatePassword.newPassword + ); + cy.get( + 'cx-my-account-v2-password button.myaccount-password-button-cancel' + ).click(); + cy.get('[formcontrolname="oldPassword"]').should('have.value', ''); + cy.get('[formcontrolname="newPassword"]').should('have.value', ''); + cy.get('[formcontrolname="newPasswordConfirm"]').should( + 'have.value', + '' + ); + }); + + it('should display server error if old password is wrong', () => { + alerts.getErrorAlert().should('not.exist'); + cy.get('[formcontrolname="oldPassword"]').type('wrongpassword'); + cy.get('[formcontrolname="newPassword"]').type( + updatePassword.newPassword + ); + cy.get('[formcontrolname="newPasswordConfirm"]').type( + updatePassword.newPassword + ); + cy.get('cx-my-account-v2-password button.btn-primary').click(); + cy.url().should('contain', updatePassword.PAGE_URL_UPDATE_PASSWORD); + alerts.getErrorAlert().should('exist'); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + after(() => { + signOutUser(); + }); + } + ); + }); +}); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-profile.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-profile.e2e.cy.ts new file mode 100644 index 00000000000..03d2a51e2fb --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/my-account/my-account-v2-profile.e2e.cy.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as updateProfile from '../../../helpers/update-profile'; +import * as login from '../../../helpers/login'; +import { viewportContext } from '../../../helpers/viewport-context'; + +describe('My Account - Update Profile', () => { + viewportContext(['mobile'], () => { + before(() => { + cy.window().then((win) => win.sessionStorage.clear()); + }); + + // Core e2e test. Repeat in mobile view. + updateProfile.testUpdateProfileLoggedInUser(); + }); + viewportContext(['desktop', 'mobile'], () => { + before(() => { + cy.window().then((win) => win.sessionStorage.clear()); + }); + + describe('update profile test for anonymous user', () => { + it('should redirect to login page for anonymous user (CXSPA-4442)', () => { + cy.visit(updateProfile.UPDATE_PROFILE_URL); + cy.location('pathname').should('contain', '/login'); + }); + }); + + describe('update profile test for logged in user', () => { + before(() => { + cy.requireLoggedIn(); + cy.visit('/'); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.selectUserMenuOption({ + option: 'Personal Details', + }); + }); + + it('should be able to change to edit mode and back (CXSPA-4442)', () => { + cy.get('.myaccount-enhancedUI-value').should('exist'); + + cy.log('--> click edit button'); + cy.get('.myaccount-enhancedUI-editButton').click(); + + cy.log('--> should show email message bar'); + cy.get('.myaccount-enhancedUI-value').should('not.exist'); + cy.get('.myaccount-enhancedUI-button-cancel').should('exist'); + + cy.log('--> click cancel button'); + cy.get('.myaccount-enhancedUI-button-cancel').click(); + + cy.log('--> should show email content'); + cy.get('.myaccount-enhancedUI-value').should('exist'); + + cy.log('--> click edit button'); + cy.get('.myaccount-enhancedUI-editButton').click(); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + after(() => { + login.signOutUser(); + }); + }); + }); +}); 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 b81bea00be2..810cc8bc5d1 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 @@ -390,6 +390,9 @@ export const tabbingOrderConfig: TabbingOrderConfig = { notificationPreference: [ { value: 'Email', type: TabbingOrderTypes.CHECKBOX_WITH_LABEL }, ], + myAccountV2NotificationPreference: [ + { value: 'Email', type: TabbingOrderTypes.CHECKBOX_WITH_LABEL }, + ], updateEmail: [ { value: 'email', type: TabbingOrderTypes.FORM_FIELD }, { value: 'confirmEmail', type: TabbingOrderTypes.FORM_FIELD }, @@ -518,6 +521,56 @@ export const tabbingOrderConfig: TabbingOrderConfig = { value: 'I approve to this sample STORE USER INFORMATION consent', }, ], + myAccountV2ConsentManagement: [ + { + type: TabbingOrderTypes.LINK, + value: 'Disable all', + }, + { + type: TabbingOrderTypes.LINK, + value: 'Enable all', + }, + { + type: TabbingOrderTypes.CHECKBOX_WITH_LABEL, + }, + { + type: TabbingOrderTypes.CHECKBOX_WITH_LABEL, + }, + { + type: TabbingOrderTypes.CHECKBOX_WITH_LABEL, + }, + ], + myAccountV2Password: [ + { + type: TabbingOrderTypes.BUTTON, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.BUTTON, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.BUTTON, + }, + { + type: TabbingOrderTypes.GENERIC_INPUT, + }, + { + type: TabbingOrderTypes.BUTTON, + }, + { + value: 'Cancel', + type: TabbingOrderTypes.BUTTON, + }, + { + value: 'Save', + type: TabbingOrderTypes.BUTTON, + }, + ], addToCart: [ { type: TabbingOrderTypes.GENERIC_BUTTON, diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-consent-management.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-consent-management.ts new file mode 100644 index 00000000000..087b046ac32 --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-consent-management.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { verifyTabbingOrder } from '../../tabbing-order'; +import { TabElement } from '../../tabbing-order.model'; + +const containerSelector = '.cx-consent-toggles'; + +export function myAccountV2consentManagementTabbingOrder(config: TabElement[]) { + cy.intercept({ + method: 'GET', + pathname: `${Cypress.env('OCC_PREFIX')}/${Cypress.env( + 'BASE_SITE' + )}/cms/components`, + }).as('getComponents'); + cy.visit('/my-account/consents'); + + cy.wait('@getComponents'); + + verifyTabbingOrder(containerSelector, config); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-notification-preference.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-notification-preference.ts new file mode 100644 index 00000000000..64a66fb5ded --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-notification-preference.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { verifyTabbingOrder } from '../../tabbing-order'; +import { TabElement } from '../../tabbing-order.model'; + +const containerSelector = '.AccountPageTemplate'; + +export function myAccountV2notificationPreferenceTabbingOrder( + config: TabElement[] +) { + cy.visit('/my-account/notification-preference'); + + verifyTabbingOrder(containerSelector, config); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-password.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-password.ts new file mode 100644 index 00000000000..177aa9c57db --- /dev/null +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/accessibility/tabbing-order/my-account/my-account-v2-password.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { verifyTabbingOrder } from '../../tabbing-order'; +import { TabElement } from '../../tabbing-order.model'; + +const containerSelector = 'cx-my-account-v2-password'; + +export function myAccountV2PasswordTabbingOrder(config: TabElement[]) { + cy.intercept({ + method: 'GET', + pathname: `${Cypress.env('OCC_PREFIX')}/${Cypress.env( + 'BASE_SITE' + )}/cms/components`, + }).as('getComponents'); + cy.visit('/my-account/update-password'); + + cy.wait('@getComponents'); + + verifyTabbingOrder(containerSelector, config); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/consent-management.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/consent-management.ts index 5f2cac6b2f6..02c1114a91a 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/consent-management.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/consent-management.ts @@ -27,6 +27,18 @@ export function giveConsent() { alerts.getSuccessAlert().should('contain', 'Consent successfully given'); } +export function giveConsentV2() { + cy.get('input[type="checkbox"]').first().should('not.be.checked'); + cy.get('input[type="checkbox"]').first().check({ force: true }); + cy.get('input[type="checkbox"]').first().should('be.checked'); + + alerts.getSuccessAlert().should('contain', 'Consent successfully given'); +} + +export function checkConsentGivenDate() { + cy.get('span[class="form-check-label description"]').contains('Approved on'); +} + export function withdrawConsent() { cy.get('input[type="checkbox"]').first().should('be.checked'); cy.get('input[type="checkbox"]').first().uncheck(); @@ -35,6 +47,14 @@ export function withdrawConsent() { alerts.getSuccessAlert().should('contain', 'Consent successfully withdrawn'); } +export function withdrawConsentV2() { + cy.get('input[type="checkbox"]').first().should('be.checked'); + cy.get('input[type="checkbox"]').first().uncheck({ force: true }); + cy.get('input[type="checkbox"]').first().should('not.be.checked'); + + alerts.getSuccessAlert().should('contain', 'Consent successfully withdrawn'); +} + export function verifyAsAnonymous() { it('should redirect to login page for anonymous user', () => { accessPageAsAnonymous(); @@ -54,3 +74,18 @@ export function consentManagementTest() { withdrawConsent(); }); } + +export function myAccountV2consentManagementTest() { + it('should be able to go to Consent Management Page', () => { + verifyConsentManagementPage(); + }); + + it('should successfully give a consent and show give data', () => { + giveConsentV2(); + checkConsentGivenDate(); + }); + + it('should successfully withdraw a consent', () => { + withdrawConsentV2(); + }); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/notification.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/notification.ts index 24f04fbb6ff..b6f0d7df32a 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/notification.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/notification.ts @@ -81,6 +81,17 @@ export function enableNotificationChannel() { .should('eq', 200); } +export function enableNotificationChannelV2() { + navigateToNotificationPreferencePage(); + const notificationPreferencesChange = + interceptNotificationPreferencesChange(); + + cy.get('[type="checkbox"]').first().check(); + cy.wait(`@${notificationPreferencesChange}`) + .its('response.statusCode') + .should('eq', 200); +} + export function disableNotificationChannel() { const notificationPreferencesChange = interceptNotificationPreferencesChange(); @@ -91,6 +102,16 @@ export function disableNotificationChannel() { .should('eq', 200); } +export function disableNotificationChannelV2() { + const notificationPreferencesChange = + interceptNotificationPreferencesChange(); + + cy.get('[type="checkbox"]').first().uncheck(); + cy.wait(`@${notificationPreferencesChange}`) + .its('response.statusCode') + .should('eq', 200); +} + export function updateEmail(): String { const password = 'Password123.'; const newUid = generateMail(randomString(), true); @@ -115,6 +136,18 @@ export function verifyEmailChannel(email: String) { cy.get('[type="checkbox"]').first().should('not.be.checked'); }); } + +export function verifyEmailChannelV2(email: String) { + navigateToNotificationPreferencePage(); + cy.get('cx-my-account-v2-notification-preference').within(() => { + cy.get('.pref-channel .form-check-label').should( + 'contain', + 'Email: ' + email + ); + cy.get('[type="checkbox"]').first().should('not.be.checked'); + }); +} + //stock notification export function verifyStockNotificationAsGuest() { navigateToPDP(normalProductCode); @@ -281,3 +314,13 @@ export function testEnableDisableNotification() { cy.get('[type="checkbox"]').first().should('not.be.checked'); }); } + +export function testEnableDisableMyAccountV2NotificationPreference() { + it('should enable/disable notification preference', () => { + enableNotificationChannelV2(); + cy.get('[type="checkbox"]').first().should('be.checked'); + + disableNotificationChannelV2(); + cy.get('[type="checkbox"]').first().should('not.be.checked'); + }); +} diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/update-email.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/update-email.ts index ba71b05aac4..c636dc6e427 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/update-email.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/update-email.ts @@ -20,7 +20,7 @@ export function registerAndLogin() { export function testUpdateEmailAndLogin() { it('should update his email address and login', () => { const newUid = generateMail(randomString(), true); - cy.get('cx-update-email').within(() => { + cy.get('cx-update-email, cx-my-account-v2-email').within(() => { cy.get('[formcontrolname="email"]').type(newUid); cy.get('[formcontrolname="confirmEmail"]').type(newUid); cy.get('[formcontrolname="password"]').type(password); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/update-password.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/update-password.ts index a9d6efeff02..8e749a4bd7a 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/update-password.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/update-password.ts @@ -18,7 +18,7 @@ export const newPassword = 'newPassword123!'; import { signOutUser } from '../helpers/login'; import { generateMail, randomString } from '../helpers/user'; -export function testUpdatePassword() { +export function testUpdatePassword(myAccountV2?: boolean) { it('should update the password with success', () => { alerts.getSuccessAlert().should('not.exist'); cy.get('[formcontrolname="oldPassword"]').type( @@ -26,7 +26,11 @@ export function testUpdatePassword() { ); cy.get('[formcontrolname="newPassword"]').type(newPassword); cy.get('[formcontrolname="newPasswordConfirm"]').type(newPassword); - cy.get('cx-update-password button.btn-primary').click(); + if (myAccountV2) { + cy.get('cx-my-account-v2-password button.btn-primary').click(); + } else { + cy.get('cx-update-password button.btn-primary').click(); + } cy.title().should('eq', PAGE_TITLE_LOGIN); alerts.getSuccessAlert().should('exist'); cy.url().should('contain', '/login'); @@ -36,7 +40,7 @@ export function testUpdatePassword() { }); } -export function testUpdatePasswordLoggedInUser() { +export function testUpdatePasswordLoggedInUser(myAccountV2?: boolean) { describe('update password test for logged in user', () => { before(() => { standardUser.registrationData.email = generateMail(randomString(), true); @@ -51,7 +55,7 @@ export function testUpdatePasswordLoggedInUser() { }); }); - testUpdatePassword(); + testUpdatePassword(myAccountV2); afterEach(() => { cy.saveLocalStorage(); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/update-profile.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/update-profile.ts index 82b07cb7e72..227c3c3c6f5 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/update-profile.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/update-profile.ts @@ -16,6 +16,9 @@ export const newLastName = 'Z'; export const UPDATE_PROFILE_URL = '/my-account/update-profile'; export function updateProfile(user?: SampleUser) { + if (Cypress.env('CX_MY_ACCOUNT_V2') === true) { + cy.get('.myaccount-enhancedUI-editButton').click(); + } if (user) { cy.get('[formcontrolname="firstName"]').should( 'have.value', @@ -23,12 +26,11 @@ export function updateProfile(user?: SampleUser) { ); cy.get('[formcontrolname="lastName"]').should('have.value', user.lastName); } - - cy.get('cx-update-profile').within(() => { + cy.get('cx-update-profile, cx-my-account-v2-profile').within(() => { cy.get('[formcontrolname="titleCode"]').ngSelect(newTitle); cy.get('[formcontrolname="firstName"]').clear().type(newFirstName); cy.get('[formcontrolname="lastName"]').clear().type(newLastName); - cy.get('button').click(); + cy.get('button.btn-primary').click(); }); // check for the global message and home screen @@ -49,7 +51,7 @@ export function validateUpdateProfileForm( firstName: string, lastName: string ) { - cy.get('cx-update-profile').within(() => { + cy.get('cx-update-profile, cx-my-account-v2-profile').within(() => { cy.get('[formcontrolname="titleCode"] .ng-value-label').should( 'have.text', title @@ -61,7 +63,10 @@ export function validateUpdateProfileForm( export function verifyUpdatedProfile() { // check where the user's details updated in the previous test - cy.get('cx-update-profile').within(() => { + if (Cypress.env('CX_MY_ACCOUNT_V2') === true) { + cy.get('.myaccount-enhancedUI-editButton').click(); + } + cy.get('cx-update-profile, cx-my-account-v2-profile').within(() => { cy.get('[formcontrolname="titleCode"] .ng-value-label').should( 'have.text', newTitle @@ -73,11 +78,14 @@ export function verifyUpdatedProfile() { export function testUpdateProfileDetails() { it('should be able to update profile details', () => { - cy.get('cx-update-profile').within(() => { + if (Cypress.env('CX_MY_ACCOUNT_V2') === true) { + cy.get('.myaccount-enhancedUI-editButton').click(); + } + cy.get('cx-update-profile, cx-my-account-v2-profile').within(() => { cy.get('[formcontrolname="titleCode"]').ngSelect(newTitle); cy.get('[formcontrolname="firstName"]').clear().type(newFirstName); cy.get('[formcontrolname="lastName"]').clear().type(newLastName); - cy.get('button').click(); + cy.get('button.btn-primary').click(); }); // check for the global message and home screen @@ -96,8 +104,11 @@ export function testUpdateProfileDetails() { export function testSeeNewProfileInfo() { it('should be able to see the new profile info', () => { + if (Cypress.env('CX_MY_ACCOUNT_V2') === true) { + cy.get('.myaccount-enhancedUI-editButton').click(); + } // check where the user's details updated in the previous test - cy.get('cx-update-profile').within(() => { + cy.get('cx-update-profile, cx-my-account-v2-profile').within(() => { cy.get('[formcontrolname="titleCode"] .ng-value-label').should( 'have.text', newTitle diff --git a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts index 1378940ea1b..b948e9a0674 100644 --- a/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts +++ b/projects/storefrontapp/src/app/spartacus/features/user/user-feature.module.ts @@ -18,10 +18,16 @@ import { userProfileTranslationChunksConfig, userProfileTranslations, } from '@spartacus/user/profile/assets'; +import { + USE_MY_ACCOUNT_V2_EMAIL, + USE_MY_ACCOUNT_V2_PASSWORD, + USE_MY_ACCOUNT_V2_PROFILE, +} from '@spartacus/user/profile/components'; import { UserProfileRootModule, USER_PROFILE_FEATURE, } from '@spartacus/user/profile/root'; +import { environment } from '../../../../environments/environment'; @NgModule({ imports: [UserAccountRootModule, UserProfileRootModule], @@ -53,6 +59,18 @@ import { }, }, }), + { + provide: USE_MY_ACCOUNT_V2_PROFILE, + useValue: environment.myAccountV2, + }, + { + provide: USE_MY_ACCOUNT_V2_EMAIL, + useValue: environment.myAccountV2, + }, + { + provide: USE_MY_ACCOUNT_V2_PASSWORD, + useValue: environment.myAccountV2, + }, provideConfig({ i18n: { resources: userProfileTranslations, diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index 1d4e07be3bb..36e97312948 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -57,6 +57,8 @@ import { StockNotificationModule, TabParagraphContainerModule, VideoModule, + USE_MY_ACCOUNT_V2_NOTIFICATION_PREFERENCE, + USE_MY_ACCOUNT_V2_CONSENT, } from '@spartacus/storefront'; import { environment } from '../../environments/environment'; import { AsmFeatureModule } from './features/asm/asm-feature.module'; @@ -241,5 +243,16 @@ if (environment.requestedDeliveryDate) { ProductConfiguratorRulebasedFeatureModule, ...featureModules, ], + providers: [ + // Adding the provider here because consents feature is not code-splitted to separate library and not lazy-loaded + { + provide: USE_MY_ACCOUNT_V2_CONSENT, + useValue: environment.myAccountV2, + }, + { + provide: USE_MY_ACCOUNT_V2_NOTIFICATION_PREFERENCE, + useValue: environment.myAccountV2, + }, + ], }) export class SpartacusFeaturesModule {} diff --git a/projects/storefrontlib/cms-components/myaccount/consent-management/consent-management.module.ts b/projects/storefrontlib/cms-components/myaccount/consent-management/consent-management.module.ts index a07eecb7203..7dc906f996c 100644 --- a/projects/storefrontlib/cms-components/myaccount/consent-management/consent-management.module.ts +++ b/projects/storefrontlib/cms-components/myaccount/consent-management/consent-management.module.ts @@ -5,19 +5,31 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { NgModule, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AuthGuard, CmsConfig, I18nModule, provideDefaultConfig, + provideDefaultConfigFactory, } from '@spartacus/core'; import { SpinnerModule } from '../../../shared/components/spinner/spinner.module'; import { IconModule } from '../../misc/icon/icon.module'; import { ConsentManagementFormComponent } from './components/consent-form/consent-management-form.component'; import { ConsentManagementComponent } from './components/consent-management.component'; import { ConsentManagementComponentService } from './consent-management-component.service'; +import { USE_MY_ACCOUNT_V2_CONSENT } from '../my-account-v2/use-my-account-v2-consent-notification-perference'; +import { MyAccountV2ConsentManagementComponent } from '../my-account-v2/my-account-v2-consent-management'; + +const myAccountV2CmsMapping: CmsConfig = { + cmsComponents: { + ConsentManagementComponent: { + component: MyAccountV2ConsentManagementComponent, + //guards: inherited from standard config, + }, + }, +}; @NgModule({ imports: [ @@ -38,6 +50,9 @@ import { ConsentManagementComponentService } from './consent-management-componen }, }, }), + provideDefaultConfigFactory(() => + inject(USE_MY_ACCOUNT_V2_CONSENT) ? myAccountV2CmsMapping : {} + ), ], declarations: [ConsentManagementComponent, ConsentManagementFormComponent], exports: [ConsentManagementComponent, ConsentManagementFormComponent], diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/index.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/index.ts index 34338a3528a..1db481487e6 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-account-v2/index.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/index.ts @@ -6,3 +6,6 @@ export * from './my-account-v2-navigation/index'; export * from './my-account-v2.module'; +export * from './my-account-v2-consent-management/index'; +export * from './my-account-v2-notification-preference/index'; +export * from './use-my-account-v2-consent-notification-perference'; diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.html b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.html new file mode 100644 index 00000000000..12d6a3dd12c --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.html @@ -0,0 +1,28 @@ + diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.spec.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.spec.ts new file mode 100644 index 00000000000..2a8a55a7df0 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.spec.ts @@ -0,0 +1,150 @@ +import { DebugElement } from '@angular/core'; +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + ANONYMOUS_CONSENT_STATUS, + ConsentTemplate, + I18nTestingModule, +} from '@spartacus/core'; +import { MyAccountV2ConsentManagementFormComponent } from './my-account-v2-consent-management-form.component'; + +describe('MyAccountV2ConsentManagementFormComponent', () => { + let component: MyAccountV2ConsentManagementFormComponent; + let fixture: ComponentFixture; + let el: DebugElement; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [I18nTestingModule], + declarations: [MyAccountV2ConsentManagementFormComponent], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent( + MyAccountV2ConsentManagementFormComponent + ); + component = fixture.componentInstance; + component.consentTemplate = {}; + component.requiredConsents = []; + component.consent = null; + el = fixture.debugElement; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component method tests', () => { + describe('ngOnInit', () => { + describe('when anonymous consents feature is enabled and consent is provided', () => { + it('should set consentGiven according to the state of the provided consent', () => { + component.consent = { consentState: ANONYMOUS_CONSENT_STATUS.GIVEN }; + component.ngOnInit(); + expect(component.consentGiven).toEqual(true); + }); + }); + describe('when a consent is given', () => { + const mockConsentTemplate: ConsentTemplate = { + id: 'TEMPLATE_ID', + currentConsent: { + consentGivenDate: new Date(), + }, + }; + it('should set internal flag to true', () => { + component.consentTemplate = mockConsentTemplate; + component.ngOnInit(); + expect(component['consentGiven']).toEqual(true); + }); + }); + describe('when a consent is withdrawn', () => { + const mockConsentTemplate: ConsentTemplate = { + currentConsent: { + consentWithdrawnDate: new Date(), + }, + }; + it('should set internal flag to false', () => { + component.consentTemplate = mockConsentTemplate; + component.ngOnInit(); + expect(component['consentGiven']).toEqual(false); + }); + }); + }); + + describe('onConsentChange', () => { + const mockConsentTemplate: ConsentTemplate = { + id: 'mock ID', + }; + it('should emit an event', () => { + const consentGiven = true; + component.consentGiven = consentGiven; + component.consentTemplate = mockConsentTemplate; + spyOn(component.consentChanged, 'emit').and.stub(); + + component.onConsentChange(); + + expect(component.consentChanged.emit).toHaveBeenCalledWith({ + given: !consentGiven, + template: mockConsentTemplate, + }); + }); + }); + + describe('isRequired', () => { + it('should return TRUE if the id is included in the required array', () => { + const templateId = 'TEMPLATE_ID'; + component.requiredConsents = [templateId, 'OTHER1', 'OTHER2']; + + expect(component.isRequired(templateId)).toBeTruthy(); + }); + it('should return FALSE if the id is NOT included in the required array', () => { + const templateId = 'TEMPLATE_ID'; + component.requiredConsents = ['OTHER1', 'OTHER2']; + + expect(component.isRequired(templateId)).toBeFalsy(); + + component.requiredConsents = []; + expect(component.isRequired(templateId)).toBeFalsy(); + }); + }); + }); + + describe('component UI tests', () => { + describe('when a checkbox is clicked', () => { + const date = new Date('2023-09-12T09:19:49+0000'); + const mockConsentTemplate: ConsentTemplate = { + id: 'mock ID', + currentConsent: { + consentGivenDate: date, + }, + }; + it('should call onConsentChange()', () => { + spyOn(component, 'onConsentChange').and.stub(); + + component.consentTemplate = mockConsentTemplate; + component.consentGiven = true; + component.ngOnInit(); + fixture.detectChanges(); + + const checkbox = el.query(By.css('input')).nativeElement as HTMLElement; + checkbox.dispatchEvent(new MouseEvent('click')); + + expect(component.onConsentChange).toHaveBeenCalled(); + }); + it('should disable required consents', () => { + component.consentTemplate = mockConsentTemplate; + component.requiredConsents = [mockConsentTemplate.id]; + + fixture.detectChanges(); + + const checkbox = el.query(By.css('input')).nativeElement as HTMLElement; + + expect(checkbox.hasAttribute('disabled')).toBeTruthy(); + }); + }); + }); +}); diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.ts new file mode 100644 index 00000000000..60464a77ba9 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/consent-form/my-account-v2-consent-management-form.component.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, OnInit } from '@angular/core'; +import { ANONYMOUS_CONSENT_STATUS } from '@spartacus/core'; +import { ConsentManagementFormComponent } from '../../../../consent-management/components/consent-form/consent-management-form.component'; + +@Component({ + selector: 'cx-my-account-v2-consent-management-form', + templateUrl: './my-account-v2-consent-management-form.component.html', +}) +export class MyAccountV2ConsentManagementFormComponent + extends ConsentManagementFormComponent + implements OnInit +{ + consentApprovedTime: string; + + ngOnInit(): void { + if (this.consent) { + this.consentGiven = Boolean( + this.consent.consentState === ANONYMOUS_CONSENT_STATUS.GIVEN + ); + } else { + if (this.consentTemplate && this.consentTemplate.currentConsent) { + if (this.consentTemplate.currentConsent.consentWithdrawnDate) { + this.consentGiven = false; + } else if (this.consentTemplate.currentConsent.consentGivenDate) { + this.consentGiven = true; + const date = new Date( + this.consentTemplate.currentConsent.consentGivenDate + ); + this.consentApprovedTime = `${date.getDate()}/${ + date.getMonth() + 1 + }/${date.getFullYear()}`; + } + } + } + } +} diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.html b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.html new file mode 100644 index 00000000000..2a52a8ff682 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.html @@ -0,0 +1,39 @@ +
+
+ +
+
+ +
+ + + +
diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.spec.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.spec.ts new file mode 100644 index 00000000000..f2d5bf64dc2 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.spec.ts @@ -0,0 +1,761 @@ +import { + Component, + DebugElement, + EventEmitter, + Input, + Output, +} from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + AnonymousConsentsConfig, + AnonymousConsentsService, + AuthService, + Consent, + ConsentTemplate, + GlobalMessageService, + GlobalMessageType, + I18nTestingModule, + Translatable, + UserConsentService, +} from '@spartacus/core'; +import { EMPTY, Observable, of } from 'rxjs'; +import { MyAccountV2ConsentManagementComponent } from './my-account-v2-consent-management.component'; +import { ConsentManagementComponentService } from '../../../consent-management'; + +@Component({ + selector: 'cx-spinner', + template: `
spinner
`, +}) +class MockCxSpinnerComponent {} + +@Component({ + selector: 'cx-my-account-v2-consent-management', + template: `
form
`, +}) +class MockConsentManagementFormComponent { + @Input() + consentTemplate: ConsentTemplate; + @Input() + requiredConsents: string[] = []; + @Output() + consentChanged = new EventEmitter<{ + given: boolean; + template: ConsentTemplate; + }>(); +} + +class UserConsentServiceMock { + loadConsents(): void {} + getConsentsResultLoading(): Observable { + return EMPTY; + } + getGiveConsentResultLoading(): Observable { + return EMPTY; + } + getGiveConsentResultSuccess(): Observable { + return EMPTY; + } + getWithdrawConsentResultLoading(): Observable { + return EMPTY; + } + getWithdrawConsentResultSuccess(): Observable { + return EMPTY; + } + getConsents(): Observable { + return EMPTY; + } + giveConsent( + _consentTemplateId: string, + _consentTemplateVersion: number + ): void {} + resetGiveConsentProcessState(): void {} + withdrawConsent(_consentCode: string): void {} + resetWithdrawConsentProcessState(): void {} + filterConsentTemplates( + _templateList: ConsentTemplate[], + _hideTemplateIds: string[] = [] + ): ConsentTemplate[] { + return []; + } + isConsentGiven(_consent: Consent): boolean { + return false; + } + isConsentWithdrawn(_consent: Consent): boolean { + return false; + } +} + +class AnonymousConsentsServiceMock { + getTemplates(): Observable { + return of([]); + } +} + +class GlobalMessageServiceMock { + add(_text: string | Translatable, _type: GlobalMessageType): void {} +} + +class AuthServiceMock { + isUserLoggedIn(): Observable { + return of(true); + } +} + +const mockConsentTemplate: ConsentTemplate = { + id: 'mock ID', + version: 0, + currentConsent: { + code: 'mock code', + }, +}; + +describe('MyAccountV2ConsentManagementComponent', () => { + let component: MyAccountV2ConsentManagementComponent; + let fixture: ComponentFixture; + let el: DebugElement; + + let userService: UserConsentService; + let globalMessageService: GlobalMessageService; + let anonymousConsentsConfig: AnonymousConsentsConfig; + let anonymousConsentsService: AnonymousConsentsService; + + beforeEach( + waitForAsync(() => { + const mockAnonymousConsentsConfig = { + anonymousConsents: {}, + }; + + TestBed.configureTestingModule({ + imports: [I18nTestingModule], + declarations: [ + MockCxSpinnerComponent, + MockConsentManagementFormComponent, + MyAccountV2ConsentManagementComponent, + ], + providers: [ + ConsentManagementComponentService, + { provide: UserConsentService, useClass: UserConsentServiceMock }, + { provide: GlobalMessageService, useClass: GlobalMessageServiceMock }, + { + provide: AnonymousConsentsService, + useClass: AnonymousConsentsServiceMock, + }, + { + provide: AuthService, + useClass: AuthServiceMock, + }, + { + provide: AnonymousConsentsConfig, + useValue: mockAnonymousConsentsConfig, + }, + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MyAccountV2ConsentManagementComponent); + component = fixture.componentInstance; + el = fixture.debugElement; + + userService = TestBed.inject(UserConsentService); + globalMessageService = TestBed.inject(GlobalMessageService); + anonymousConsentsConfig = TestBed.inject(AnonymousConsentsConfig); + anonymousConsentsService = TestBed.inject(AnonymousConsentsService); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + const consentListInitMethod = 'consentListInit'; + const giveConsentInitMethod = 'giveConsentInit'; + const withdrawConsentInitMethod = 'withdrawConsentInit'; + const consentsExistsMethod = 'consentsExists'; + const onConsentGivenSuccessMethod = 'onConsentGivenSuccess'; + const onConsentWithdrawnSuccessMethod = 'onConsentWithdrawnSuccess'; + const hideAnonymousConsentsMethod = 'hideAnonymousConsents'; + + describe('component method tests', () => { + describe('ngOnInit', () => { + it('should combine all loading flags into one', () => { + spyOn(userService, 'getConsentsResultLoading').and.returnValue( + of(true) + ); + spyOn(userService, 'getGiveConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(false) + ); + + component.ngOnInit(); + expect(userService.getConsentsResultLoading).toHaveBeenCalled(); + expect(userService.getGiveConsentResultLoading).toHaveBeenCalled(); + expect(userService.getWithdrawConsentResultLoading).toHaveBeenCalled(); + + let loadingResult = false; + component.loading$ + .subscribe((result) => (loadingResult = result)) + .unsubscribe(); + expect(loadingResult).toEqual(true); + }); + + it('should call all init methods', () => { + spyOn(component, consentListInitMethod).and.stub(); + spyOn(component, giveConsentInitMethod).and.stub(); + spyOn(component, withdrawConsentInitMethod).and.stub(); + + component.ngOnInit(); + expect(component[consentListInitMethod]).toHaveBeenCalled(); + expect(component[giveConsentInitMethod]).toHaveBeenCalled(); + expect(component[withdrawConsentInitMethod]).toHaveBeenCalled(); + }); + }); + + describe(consentListInitMethod, () => { + describe('when there are no consents loaded', () => { + const mockTemplateList = [] as ConsentTemplate[]; + it('should trigger the loadConsents method', () => { + spyOn(userService, 'getConsents').and.returnValue( + of(mockTemplateList) + ); + spyOn(component, consentsExistsMethod).and.returnValue(false); + spyOn(userService, 'loadConsents').and.stub(); + + component[consentListInitMethod](); + + let result: ConsentTemplate[]; + component.templateList$ + .subscribe((templates) => (result = templates)) + .unsubscribe(); + expect(result).toEqual(mockTemplateList); + expect(component[consentsExistsMethod]).toHaveBeenCalledWith( + mockTemplateList + ); + expect(userService.loadConsents).toHaveBeenCalled(); + }); + }); + describe('when the consents are already present', () => { + const mockTemplateList: ConsentTemplate[] = [mockConsentTemplate]; + it('should not trigger loading of consents and should return consent template list', () => { + spyOn(userService, 'getConsents').and.returnValue( + of(mockTemplateList) + ); + spyOn(component, consentsExistsMethod).and.returnValue(true); + spyOn(userService, 'loadConsents').and.stub(); + + component[consentListInitMethod](); + + let result: ConsentTemplate[]; + component.templateList$ + .subscribe((templates) => (result = templates)) + .unsubscribe(); + expect(result).toEqual(mockTemplateList); + expect(component[consentsExistsMethod]).toHaveBeenCalledWith( + mockTemplateList + ); + expect(userService.loadConsents).not.toHaveBeenCalled(); + }); + }); + describe('when the anonymousConsents.consentManagementPage config is defined', () => { + it(`should call ${hideAnonymousConsentsMethod} method`, () => { + const mockTemplateList: ConsentTemplate[] = [mockConsentTemplate]; + spyOn(userService, 'getConsents').and.returnValue( + of(mockTemplateList) + ); + spyOn(component, hideAnonymousConsentsMethod).and.returnValue( + mockTemplateList + ); + const mockAnonymousConsentTemplates: ConsentTemplate[] = [ + { id: 'MARKETING' }, + ]; + spyOn(anonymousConsentsService, 'getTemplates').and.returnValue( + of(mockAnonymousConsentTemplates) + ); + anonymousConsentsConfig.anonymousConsents.consentManagementPage = {}; + + component[consentListInitMethod](); + + let result: ConsentTemplate[]; + component.templateList$ + .subscribe((templates) => (result = templates)) + .unsubscribe(); + expect(result).toEqual(mockTemplateList); + expect(anonymousConsentsService.getTemplates).toHaveBeenCalled(); + expect(component[hideAnonymousConsentsMethod]).toHaveBeenCalledWith( + mockTemplateList, + mockAnonymousConsentTemplates + ); + }); + }); + }); + + describe(giveConsentInitMethod, () => { + it('should reset the processing state', () => { + spyOn(userService, 'resetGiveConsentProcessState').and.stub(); + component[giveConsentInitMethod](); + expect(userService.resetGiveConsentProcessState).toHaveBeenCalled(); + }); + it(`should call ${onConsentGivenSuccessMethod}`, () => { + const success = true; + spyOn(userService, 'getGiveConsentResultSuccess').and.returnValue( + of(success) + ); + spyOn(component, onConsentGivenSuccessMethod).and.stub(); + + component[giveConsentInitMethod](); + expect(component[onConsentGivenSuccessMethod]).toHaveBeenCalledWith( + success + ); + }); + }); + + describe(withdrawConsentInitMethod, () => { + it('should reset the processing state', () => { + spyOn(userService, 'resetWithdrawConsentProcessState').and.stub(); + component[withdrawConsentInitMethod](); + expect(userService.resetWithdrawConsentProcessState).toHaveBeenCalled(); + }); + it(`should load all consents if the withdrawal was successful and call ${onConsentWithdrawnSuccessMethod}`, () => { + const withdrawalSuccess = true; + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getWithdrawConsentResultSuccess').and.returnValue( + of(withdrawalSuccess) + ); + spyOn(userService, 'loadConsents').and.stub(); + spyOn(component, onConsentWithdrawnSuccessMethod).and.stub(); + + component[withdrawConsentInitMethod](); + + expect(userService.loadConsents).toHaveBeenCalled(); + expect(component[onConsentWithdrawnSuccessMethod]).toHaveBeenCalledWith( + withdrawalSuccess + ); + }); + it('should NOT load all consents if the withdrawal was NOT successful', () => { + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getWithdrawConsentResultSuccess').and.returnValue( + of(false) + ); + spyOn(userService, 'loadConsents').and.stub(); + + component[withdrawConsentInitMethod](); + + expect(userService.loadConsents).not.toHaveBeenCalled(); + }); + }); + + describe(consentsExistsMethod, () => { + describe('when undefined is provided', () => { + it('should return false', () => { + expect(component[consentsExistsMethod](undefined)).toEqual(false); + }); + }); + describe('when consentTemplates do not exist', () => { + it('should return false', () => { + const consentTemplateList = {} as ConsentTemplate[]; + expect(component[consentsExistsMethod](consentTemplateList)).toEqual( + false + ); + }); + }); + describe('when consentTemplates are an empty array', () => { + it('should return false', () => { + const consentTemplateList: ConsentTemplate[] = []; + expect(component[consentsExistsMethod](consentTemplateList)).toEqual( + false + ); + }); + }); + describe('when consentTemplates are present', () => { + it('should return true', () => { + const consentTemplateList: ConsentTemplate[] = [mockConsentTemplate]; + expect(component[consentsExistsMethod](consentTemplateList)).toEqual( + true + ); + }); + }); + }); + + describe('onConsentChange', () => { + describe('when the consent was given', () => { + it('should call facades giveConsent method', () => { + spyOn(userService, 'giveConsent').and.stub(); + spyOn(userService, 'withdrawConsent').and.stub(); + + component.onConsentChange({ + given: true, + template: mockConsentTemplate, + }); + + expect(userService.giveConsent).toHaveBeenCalledWith( + mockConsentTemplate.id, + mockConsentTemplate.version + ); + expect(userService.withdrawConsent).not.toHaveBeenCalled(); + }); + }); + describe('when the consent was NOT given', () => { + it('should call facades withdrawConsent method', () => { + spyOn(userService, 'giveConsent').and.stub(); + spyOn(userService, 'withdrawConsent').and.stub(); + + component.onConsentChange({ + given: false, + template: mockConsentTemplate, + }); + + expect(userService.withdrawConsent).toHaveBeenCalledWith( + mockConsentTemplate.currentConsent.code, + mockConsentTemplate.id + ); + expect(userService.giveConsent).not.toHaveBeenCalled(); + }); + }); + }); + + describe(onConsentGivenSuccessMethod, () => { + describe('when the consent was NOT successfully given', () => { + it('should NOT reset the processing state and display a success message', () => { + spyOn(userService, 'resetGiveConsentProcessState').and.stub(); + spyOn(globalMessageService, 'add').and.stub(); + + component[onConsentGivenSuccessMethod](false); + + expect( + userService.resetGiveConsentProcessState + ).not.toHaveBeenCalled(); + expect(globalMessageService.add).not.toHaveBeenCalled(); + }); + }); + describe('when the consent was successfully given', () => { + it('should reset the processing state and display a success message', () => { + spyOn(userService, 'resetGiveConsentProcessState').and.stub(); + spyOn(globalMessageService, 'add').and.stub(); + + component[onConsentGivenSuccessMethod](true); + + expect(userService.resetGiveConsentProcessState).toHaveBeenCalled(); + expect(globalMessageService.add).toHaveBeenCalledWith( + { key: 'consentManagementForm.message.success.given' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + }); + }); + }); + + describe(onConsentWithdrawnSuccessMethod, () => { + describe('when the consent was NOT successfully withdrawn', () => { + it('should NOT reset the processing state and display a success message', () => { + spyOn(userService, 'resetWithdrawConsentProcessState').and.stub(); + spyOn(globalMessageService, 'add').and.stub(); + + component[onConsentWithdrawnSuccessMethod](false); + + expect( + userService.resetWithdrawConsentProcessState + ).not.toHaveBeenCalled(); + expect(globalMessageService.add).not.toHaveBeenCalled(); + }); + }); + describe('when the consent was successfully withdrawn', () => { + it('should reset the processing state and display a success message', () => { + spyOn(userService, 'resetWithdrawConsentProcessState').and.stub(); + spyOn(globalMessageService, 'add').and.stub(); + + component[onConsentWithdrawnSuccessMethod](true); + + expect( + userService.resetWithdrawConsentProcessState + ).toHaveBeenCalled(); + expect(globalMessageService.add).toHaveBeenCalledWith( + { key: 'consentManagementForm.message.success.withdrawn' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + }); + }); + }); + + const isRequiredConsentMethod = 'isRequiredConsent'; + describe(isRequiredConsentMethod, () => { + describe('when the requiredConsents is NOT configured', () => { + it('should return false', () => { + anonymousConsentsConfig.anonymousConsents.requiredConsents = + undefined; + const result = + component[isRequiredConsentMethod](mockConsentTemplate); + expect(result).toEqual(false); + }); + }); + describe('when the requiredConsents is configured', () => { + it('should return true', () => { + anonymousConsentsConfig.anonymousConsents.requiredConsents = [ + mockConsentTemplate.id, + ]; + const result = + component[isRequiredConsentMethod](mockConsentTemplate); + expect(result).toEqual(true); + }); + }); + }); + + describe('rejectAll', () => { + describe('when no consent is given', () => { + it('should not call userConsentService.withdrawConsent', () => { + spyOn(userService, 'withdrawConsent').and.stub(); + spyOn(userService, 'loadConsents').and.stub(); + component.rejectAll([]); + expect(userService.withdrawConsent).not.toHaveBeenCalled(); + }); + }); + describe('when consents are given', () => { + it('should call userConsentService.withdrawConsent for each', () => { + spyOn(userService, 'withdrawConsent').and.stub(); + spyOn(userService, 'isConsentGiven').and.returnValue(true); + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(false) + ); + + component.rejectAll([mockConsentTemplate]); + + expect(userService.withdrawConsent).toHaveBeenCalledWith( + mockConsentTemplate.currentConsent.code, + mockConsentTemplate.id + ); + expect(userService.withdrawConsent).toHaveBeenCalledTimes(1); + }); + }); + describe('when the required consents are configured', () => { + it('should skip them', () => { + anonymousConsentsConfig.anonymousConsents.requiredConsents = [ + mockConsentTemplate[0], + ]; + spyOn(userService, 'withdrawConsent').and.stub(); + spyOn(userService, 'loadConsents').and.stub(); + spyOn(userService, 'isConsentGiven').and.returnValue(true); + spyOn(component, isRequiredConsentMethod).and.returnValue(true); + + component.rejectAll([mockConsentTemplate]); + + expect(userService.withdrawConsent).not.toHaveBeenCalled(); + }); + }); + }); + + describe('allowAll', () => { + describe('when no consent is withdrawn', () => { + it('should not call userConsentService.giveConsent', () => { + spyOn(userService, 'giveConsent').and.stub(); + spyOn(userService, 'loadConsents').and.stub(); + component.allowAll([]); + expect(userService.giveConsent).not.toHaveBeenCalled(); + }); + }); + describe('when consents are withdrawn', () => { + it('should call userConsentService.giveConsent for each', () => { + spyOn(userService, 'giveConsent').and.stub(); + spyOn(userService, 'isConsentWithdrawn').and.returnValue(true); + spyOn(userService, 'getGiveConsentResultLoading').and.returnValue( + of(false) + ); + + component.allowAll([mockConsentTemplate]); + + expect(userService.giveConsent).toHaveBeenCalledWith( + mockConsentTemplate.id, + mockConsentTemplate.version + ); + expect(userService.giveConsent).toHaveBeenCalledTimes(1); + }); + }); + describe('when the required consents are configured', () => { + it('should skip them', () => { + anonymousConsentsConfig.anonymousConsents.requiredConsents = [ + mockConsentTemplate[0], + ]; + spyOn(userService, 'giveConsent').and.stub(); + spyOn(userService, 'loadConsents').and.stub(); + spyOn(userService, 'isConsentWithdrawn').and.returnValue(true); + spyOn(component, isRequiredConsentMethod).and.returnValue(true); + + component.allowAll([mockConsentTemplate]); + + expect(userService.giveConsent).not.toHaveBeenCalled(); + }); + }); + }); + + describe('ngOnDestroy', () => { + it('should unsubscribe and reset the processing states', () => { + spyOn(component['subscriptions'], 'unsubscribe').and.stub(); + spyOn(userService, 'resetGiveConsentProcessState').and.stub(); + spyOn(userService, 'resetWithdrawConsentProcessState').and.stub(); + + component.ngOnDestroy(); + + expect(component['subscriptions'].unsubscribe).toHaveBeenCalled(); + expect(userService.resetGiveConsentProcessState).toHaveBeenCalled(); + expect(userService.resetWithdrawConsentProcessState).toHaveBeenCalled(); + }); + }); + + describe(hideAnonymousConsentsMethod, () => { + const mockConsentTemplates = [mockConsentTemplate]; + const anonymousTemplates: ConsentTemplate[] = [{ id: 'MARKETING' }]; + const hideConsents: string[] = ['MARKETING']; + describe('when the showAnonymousConsents config is false', () => { + it('should filter with the provided anonymousTemplates', () => { + anonymousConsentsConfig.anonymousConsents.consentManagementPage = { + showAnonymousConsents: false, + hideConsents, + }; + spyOn(userService, 'filterConsentTemplates').and.returnValue( + mockConsentTemplates + ); + + const result = component[hideAnonymousConsentsMethod]( + mockConsentTemplates, + anonymousTemplates + ); + expect(result).toEqual(mockConsentTemplates); + expect(userService.filterConsentTemplates).toHaveBeenCalledWith( + mockConsentTemplates, + hideConsents + ); + }); + }); + describe('when the showAnonymousConsents config is true', () => { + it('should check hideConsents config and filter with provided hideConsents', () => { + anonymousConsentsConfig.anonymousConsents.consentManagementPage = { + showAnonymousConsents: true, + hideConsents, + }; + spyOn(userService, 'filterConsentTemplates').and.returnValue( + mockConsentTemplates + ); + + const result = component[hideAnonymousConsentsMethod]( + mockConsentTemplates, + anonymousTemplates + ); + expect(result).toEqual(mockConsentTemplates); + expect(userService.filterConsentTemplates).toHaveBeenCalledWith( + mockConsentTemplates, + hideConsents + ); + expect(userService.filterConsentTemplates).toHaveBeenCalledWith( + mockConsentTemplates, + hideConsents + ); + }); + }); + }); + }); + + describe('component UI tests', () => { + describe('spinner', () => { + describe('when consents are loading', () => { + it('should show spinner', () => { + spyOn(userService, 'getConsentsResultLoading').and.returnValue( + of(true) + ); + spyOn(userService, 'getGiveConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(component, consentListInitMethod).and.stub(); + spyOn(component, giveConsentInitMethod).and.stub(); + spyOn(component, withdrawConsentInitMethod).and.stub(); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + }); + describe('when a consent is being given', () => { + it('should show spinner', () => { + spyOn(userService, 'getConsentsResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getGiveConsentResultLoading').and.returnValue( + of(true) + ); + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(component, consentListInitMethod).and.stub(); + spyOn(component, giveConsentInitMethod).and.stub(); + spyOn(component, withdrawConsentInitMethod).and.stub(); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + }); + describe('when a consent is being withdrawn', () => { + it('should show spinner', () => { + spyOn(userService, 'getConsentsResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getGiveConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(true) + ); + spyOn(component, consentListInitMethod).and.stub(); + spyOn(component, giveConsentInitMethod).and.stub(); + spyOn(component, withdrawConsentInitMethod).and.stub(); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + }); + + describe('when nothing is being loaded', () => { + it('should NOT show the spinner but rather diplay a checkbox for each consent', () => { + spyOn(userService, 'getConsentsResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getGiveConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getWithdrawConsentResultLoading').and.returnValue( + of(false) + ); + spyOn(userService, 'getConsents').and.returnValue( + of([ + mockConsentTemplate, + mockConsentTemplate, + mockConsentTemplate, + ] as ConsentTemplate[]) + ); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(el.query(By.css('cx-spinner'))).toBeFalsy(); + expect( + (el.nativeElement as HTMLElement).querySelectorAll( + 'cx-my-account-v2-consent-management-form' + ).length + ).toEqual(3); + }); + }); + }); + }); +}); diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.ts new file mode 100644 index 00000000000..fc7b34f3e51 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/components/my-account-v2-consent-management.component.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component } from '@angular/core'; +import { ConsentManagementComponent } from '../../../consent-management/components/consent-management.component'; + +@Component({ + selector: 'cx-my-account-v2-consent-management', + templateUrl: './my-account-v2-consent-management.component.html', +}) +export class MyAccountV2ConsentManagementComponent extends ConsentManagementComponent {} diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/index.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/index.ts new file mode 100644 index 00000000000..c51abfaac02 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/index.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './components/consent-form/my-account-v2-consent-management-form.component'; +export * from './components/my-account-v2-consent-management.component'; +export * from './my-account-v2-consent-management.module'; diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/my-account-v2-consent-management.module.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/my-account-v2-consent-management.module.ts new file mode 100644 index 00000000000..9a5d0f6243f --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-consent-management/my-account-v2-consent-management.module.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * 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 { I18nModule } from '@spartacus/core'; +import { SpinnerModule } from '../../../../shared/components/spinner/spinner.module'; +import { IconModule } from '../../../misc/icon/icon.module'; +import { MyAccountV2ConsentManagementComponent } from './components/my-account-v2-consent-management.component'; +import { MyAccountV2ConsentManagementFormComponent } from './components/consent-form/my-account-v2-consent-management-form.component'; +import { ConsentManagementComponentService } from '../../consent-management'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SpinnerModule, + I18nModule, + IconModule, + ], + providers: [ConsentManagementComponentService], + declarations: [ + MyAccountV2ConsentManagementComponent, + MyAccountV2ConsentManagementFormComponent, + ], + exports: [ + MyAccountV2ConsentManagementComponent, + MyAccountV2ConsentManagementFormComponent, + ], +}) +export class MyAccountV2ConsentManagementModule {} diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/index.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/index.ts new file mode 100644 index 00000000000..74a80738c57 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './my-account-v2-notification-preference.component'; +export * from './my-account-v2-notification-preference.module'; diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.component.html b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.component.html new file mode 100644 index 00000000000..8b7a961a776 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.component.html @@ -0,0 +1,48 @@ + +
+
+
+
+
+ {{ 'myAccountV2NotifiationPerference.header' | cxTranslate }} +
+
+ {{ 'myAccountV2NotifiationPerference.message' | cxTranslate }} +
+
+ + + +
+ +
+
+
+ + +
+ +
+
+
diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.component.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.component.ts new file mode 100644 index 00000000000..b058d4a6d8d --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.component.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NotificationPreferenceComponent } from '../../notification-preference/notification-preference.component'; + +@Component({ + selector: 'cx-my-account-v2-notification-preference', + templateUrl: './my-account-v2-notification-preference.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyAccountV2NotificationPreferenceComponent extends NotificationPreferenceComponent {} diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.module.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.module.ts new file mode 100644 index 00000000000..98ecb55e58a --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.module.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * 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 { AuthGuard, I18nModule } from '@spartacus/core'; +import { SpinnerModule } from '../../../../shared/components/spinner/spinner.module'; +import { CmsPageGuard } from '../../../../cms-structure/guards/cms-page.guard'; +import { PageLayoutComponent } from '../../../../cms-structure/page/page-layout/page-layout.component'; +import { MyAccountV2NotificationPreferenceComponent } from './my-account-v2-notification-preference.component'; + +@NgModule({ + declarations: [MyAccountV2NotificationPreferenceComponent], + imports: [ + CommonModule, + SpinnerModule, + I18nModule, + RouterModule.forChild([ + { + // @ts-ignore + path: null, + canActivate: [AuthGuard, CmsPageGuard], + component: PageLayoutComponent, + data: { cxRoute: 'notificationPreference' }, + }, + ]), + ], + exports: [MyAccountV2NotificationPreferenceComponent], +}) +export class MyAccountV2NotificationPreferenceModule {} diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.spec.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.spec.ts new file mode 100644 index 00000000000..f3194154940 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/my-account-v2-notification-preference/my-account-v2-notification-preference.spec.ts @@ -0,0 +1,156 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MyAccountV2NotificationPreferenceComponent } from './my-account-v2-notification-preference.component'; +import { + I18nTestingModule, + NotificationPreference, + UserNotificationPreferenceService, +} from '@spartacus/core'; +import { of } from 'rxjs'; +import { DebugElement, Component } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { cold, getTestScheduler } from 'jasmine-marbles'; + +@Component({ + selector: 'cx-spinner', + template: `
spinner
`, +}) +class MockCxSpinnerComponent {} + +describe('MyAccountV2NotificationPreferenceComponent', () => { + let component: MyAccountV2NotificationPreferenceComponent; + let fixture: ComponentFixture; + let el: DebugElement; + + const notificationPreferenceService = jasmine.createSpyObj( + 'UserNotificationPreferenceService', + [ + 'getPreferences', + 'loadPreferences', + 'getPreferencesLoading', + 'updatePreferences', + 'getUpdatePreferencesResultLoading', + 'resetNotificationPreferences', + ] + ); + + const notificationPreference: NotificationPreference[] = [ + { + channel: 'EMAIL', + enabled: true, + value: 'test.user@sap.com', + visible: true, + }, + { + channel: 'SMS', + enabled: false, + value: '01234567890', + visible: true, + }, + ]; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [I18nTestingModule], + declarations: [ + MyAccountV2NotificationPreferenceComponent, + MockCxSpinnerComponent, + ], + providers: [ + { + provide: UserNotificationPreferenceService, + useValue: notificationPreferenceService, + }, + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent( + MyAccountV2NotificationPreferenceComponent + ); + el = fixture.debugElement; + component = fixture.componentInstance; + + notificationPreferenceService.loadPreferences.and.stub(); + notificationPreferenceService.updatePreferences.and.stub(); + notificationPreferenceService.getPreferences.and.returnValue( + of(notificationPreference) + ); + notificationPreferenceService.getPreferencesLoading.and.returnValue( + of(false) + ); + notificationPreferenceService.getUpdatePreferencesResultLoading.and.returnValue( + of(false) + ); + notificationPreferenceService.resetNotificationPreferences.and.stub(); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should show channels', () => { + fixture.detectChanges(); + expect(el.query(By.css('.header'))).toBeTruthy(); + expect(el.query(By.css('.pref-info'))).toBeTruthy(); + expect( + el.queryAll(By.css('.form-check-input')).length === + notificationPreference.length + ).toBeTruthy(); + expect( + el.queryAll(By.css('.pref-channel')).length === + notificationPreference.length + ).toBeTruthy(); + }); + + it('should show spinner when loading', () => { + notificationPreferenceService.getPreferences.and.returnValue(of([])); + fixture.detectChanges(); + expect(el.query(By.css('cx-spinner'))).toBeTruthy(); + }); + + it('should be able to disable a channel when get loading', () => { + notificationPreferenceService.getUpdatePreferencesResultLoading.and.returnValue( + of(false) + ); + notificationPreferenceService.getPreferencesLoading.and.returnValue( + cold('-a|', { a: true }) + ); + fixture.detectChanges(); + + const cheboxies = el.queryAll(By.css('.form-check-input')); + expect(cheboxies.length).toEqual(notificationPreference.length); + const chx = cheboxies[0].nativeElement; + chx.click(); + + getTestScheduler().flush(); + fixture.detectChanges(); + + expect(notificationPreferenceService.updatePreferences).toHaveBeenCalled(); + expect(chx.disabled).toEqual(true); + }); + + it('should be able to disable a channel when update loading', () => { + notificationPreferenceService.getPreferencesLoading.and.returnValue( + of(false) + ); + notificationPreferenceService.getUpdatePreferencesResultLoading.and.returnValue( + cold('-a|', { a: true }) + ); + fixture.detectChanges(); + + const cheboxies = el.queryAll(By.css('.form-check-input')); + expect(cheboxies.length).toEqual(notificationPreference.length); + const chx = cheboxies[0].nativeElement; + chx.click(); + + getTestScheduler().flush(); + fixture.detectChanges(); + + expect(notificationPreferenceService.updatePreferences).toHaveBeenCalled(); + expect(chx.disabled).toEqual(true); + }); +}); diff --git a/projects/storefrontlib/cms-components/myaccount/my-account-v2/use-my-account-v2-consent-notification-perference.ts b/projects/storefrontlib/cms-components/myaccount/my-account-v2/use-my-account-v2-consent-notification-perference.ts new file mode 100644 index 00000000000..9fd162949f2 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-account-v2/use-my-account-v2-consent-notification-perference.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InjectionToken } from '@angular/core'; + +export const USE_MY_ACCOUNT_V2_NOTIFICATION_PREFERENCE = + new InjectionToken( + 'feature flag to enable enhanced UI for notification preference pages under My-Account', + { providedIn: 'root', factory: () => false } + ); + +export const USE_MY_ACCOUNT_V2_CONSENT = new InjectionToken( + 'feature flag to enable enhanced UI for Consent Management pages under My-Account', + { providedIn: 'root', factory: () => false } +); diff --git a/projects/storefrontlib/cms-components/myaccount/notification-preference/notification-preference.module.ts b/projects/storefrontlib/cms-components/myaccount/notification-preference/notification-preference.module.ts index 1fb28d7f49c..0fcddb46cbb 100644 --- a/projects/storefrontlib/cms-components/myaccount/notification-preference/notification-preference.module.ts +++ b/projects/storefrontlib/cms-components/myaccount/notification-preference/notification-preference.module.ts @@ -5,19 +5,30 @@ */ import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { NgModule, inject } from '@angular/core'; import { RouterModule } from '@angular/router'; import { AuthGuard, CmsConfig, I18nModule, provideDefaultConfig, + provideDefaultConfigFactory, } from '@spartacus/core'; import { CmsPageGuard } from '../../../cms-structure/guards/cms-page.guard'; import { PageLayoutComponent } from '../../../cms-structure/page/page-layout/page-layout.component'; import { SpinnerModule } from '../../../shared/components/spinner/spinner.module'; import { NotificationPreferenceComponent } from './notification-preference.component'; +import { USE_MY_ACCOUNT_V2_NOTIFICATION_PREFERENCE } from '../my-account-v2/use-my-account-v2-consent-notification-perference'; +import { MyAccountV2NotificationPreferenceComponent } from '../my-account-v2'; +const myAccountV2CmsMapping: CmsConfig = { + cmsComponents: { + NotificationPreferenceComponent: { + component: MyAccountV2NotificationPreferenceComponent, + //guards: inherited from standard config, + }, + }, +}; @NgModule({ declarations: [NotificationPreferenceComponent], imports: [ @@ -43,6 +54,11 @@ import { NotificationPreferenceComponent } from './notification-preference.compo }, }, }), + provideDefaultConfigFactory(() => + inject(USE_MY_ACCOUNT_V2_NOTIFICATION_PREFERENCE) + ? myAccountV2CmsMapping + : {} + ), ], exports: [NotificationPreferenceComponent], }) diff --git a/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-consent-management-form.scss b/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-consent-management-form.scss new file mode 100644 index 00000000000..f9f2e5a803f --- /dev/null +++ b/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-consent-management-form.scss @@ -0,0 +1,20 @@ +%cx-my-account-v2-consent-management-form { + .name { + @include type('6'); + display: block; + margin-bottom: 0.2rem; + } + + .description { + @include type(); + display: block; + } + + .consent-container { + display: block; + } + + .checkbox-input { + top: -0.2rem; + } +} diff --git a/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-consent-management.scss b/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-consent-management.scss new file mode 100644 index 00000000000..895d6ebf0cd --- /dev/null +++ b/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-consent-management.scss @@ -0,0 +1,28 @@ +%cx-my-account-v2-consent-management { + .cx-consent-toggles { + display: flex; + justify-content: center; + } + + .header { + @include type('3'); + margin-bottom: 1rem; + } + + .message { + @include type(); + margin-bottom: 1rem; + } + + .consent-form-container { + max-width: 56rem; + } + + .cx-consent-action-links { + text-align: end; + margin: 0 0 1rem; + .cx-action-link { + @include type('6'); + } + } +} diff --git a/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-notification-preference.scss b/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-notification-preference.scss new file mode 100644 index 00000000000..22796de0879 --- /dev/null +++ b/projects/storefrontstyles/scss/components/myaccount/_cx-my-account-v2-notification-preference.scss @@ -0,0 +1,52 @@ +@import '../../theme'; + +%cx-my-account-v2-notification-preference { + .np-content-center { + -ms-flex-pack: center; + justify-content: center; + } + + .np-flex { + display: -ms-flexbox; + display: flex; + } + + .np-row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + right: -1rem; + left: -1rem; + } + + .header { + @include type('3'); + margin-bottom: 1.75rem; + } + + .pref-info { + @include type(); + margin-bottom: 0.7rem; + } + + .notification-channels { + display: flex; + align-items: center; + } + + .check-box { + width: 1.5rem; + height: 1.5rem; + margin-top: 0.125rem; + } + + .check-label { + @include type('5'); + margin-top: 0.625rem; + } + + .note { + @include type('5'); + } +} diff --git a/projects/storefrontstyles/scss/components/myaccount/_index.scss b/projects/storefrontstyles/scss/components/myaccount/_index.scss index 0f4c3d39376..ba7a3d4de36 100644 --- a/projects/storefrontstyles/scss/components/myaccount/_index.scss +++ b/projects/storefrontstyles/scss/components/myaccount/_index.scss @@ -4,12 +4,17 @@ @import './payment-methods'; @import './consent-management'; @import './consent-management-form'; +@import './cx-my-account-v2-consent-management'; +@import './cx-my-account-v2-consent-management-form'; @import './my-coupons'; @import './my-coupons-card'; @import './my-coupons-dialog'; @import './my-interests'; +@import './cx-my-account-v2-notification-preference'; $myaccount-components-allowlist: cx-anonymous-consent-management-banner, cx-anonymous-consent-dialog, cx-anonymous-consent-open-dialog, cx-consent-management-form, cx-consent-management, cx-my-interests, - cx-my-coupons, cx-coupon-card, cx-coupon-dialog, cx-payment-methods !default; + cx-my-coupons, cx-coupon-card, cx-coupon-dialog, cx-payment-methods, + cx-my-account-v2-notification-preference, + cx-my-account-v2-consent-management-form, cx-my-account-v2-consent-management !default;