Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: As a customer, I want to view and update my preferred notification channels #17843

Open
wants to merge 23 commits into
base: develop-6.7.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
73d66d8
New Notification Preference Component
Melody-zhou-512 Sep 19, 2023
eda046a
Changes from code review comments
Melody-zhou-512 Sep 19, 2023
3aaebae
add cms config
Melody-zhou-512 Sep 20, 2023
f86aa98
Merge branch 'develop-6.5.x' into feature/CXSPA-4468
Melody-zhou-512 Sep 20, 2023
ffd2a17
Adapt codes as comments
Melody-zhou-512 Sep 22, 2023
47e8f9b
adapt codes as some checks
Melody-zhou-512 Sep 22, 2023
4135a20
Merge branch 'develop-6.5.x' into feature/CXSPA-4468
Melody-zhou-512 Sep 22, 2023
8dd9b10
Merge remote-tracking branch 'origin/develop-6.7.x' into feature/CXSP…
Melody-zhou-512 Nov 22, 2023
5abb88b
add feature flag
Melody-zhou-512 Nov 24, 2023
a36c3f0
add feature flags
Melody-zhou-512 Nov 24, 2023
562a124
Merge remote-tracking branch 'origin/develop-6.7.x' into feature/CXSP…
Melody-zhou-512 Nov 24, 2023
70eb5c2
fix issue
Melody-zhou-512 Nov 24, 2023
1b3eeac
add feature flag
Melody-zhou-512 Nov 24, 2023
c827cb5
fix issue
Melody-zhou-512 Nov 24, 2023
9eed9e2
fix sonar issue
Melody-zhou-512 Nov 24, 2023
352025f
fix issue
Melody-zhou-512 Nov 24, 2023
3a9acd3
Merge remote-tracking branch 'origin/develop-6.7.x' into feature/CXSP…
Melody-zhou-512 Nov 27, 2023
3e1b125
fix check issue
Melody-zhou-512 Nov 27, 2023
3fc180e
add e2e test
Melody-zhou-512 Nov 28, 2023
979710c
Merge remote-tracking branch 'origin/develop-6.7.x' into feature/CXSP…
Melody-zhou-512 Nov 28, 2023
430e520
fix sonar issue
Melody-zhou-512 Nov 28, 2023
996641f
fix e2e test issue
Melody-zhou-512 Nov 28, 2023
5260493
Merge remote-tracking branch 'origin/develop-6.7.x' into feature/CXSP…
Melody-zhou-512 Nov 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions projects/assets/src/translations/en/my-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const myAccount = {
EMAIL: 'Email:',
SMS: 'SMS:',
SITE_MESSAGE: 'SiteMessage',
channels: 'Notification Channels',
},
myInterests: {
header: 'My Interests',
Expand Down
1 change: 1 addition & 0 deletions projects/storefrontlib/cms-components/myaccount/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * from './my-coupons/index';
export * from './my-interests/my-interests.component';
export * from './my-interests/my-interests.module';
export * from './notification-preference/index';
export * from './new-notification-preference/index';
export * from './payment-methods/payment-methods.component';
export * from './payment-methods/payment-methods.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

export * from './new-notification-preference.component';
export * from './new-notification-preference.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<ng-container *ngIf="preferences$ | async as preferences">
<div *ngIf="preferences.length > 0; else loading">
<div role="status" [attr.aria-label]="'common.loaded' | cxTranslate"></div>
<div class="row d-flex justify-content-center">
Zeyber marked this conversation as resolved.
Show resolved Hide resolved
<div class="col-md-8">
<div class="header">
{{ 'notificationPreference.channels' | cxTranslate }}
</div>
<div class="pref-info">
{{ 'notificationPreference.message' | cxTranslate }}
</div>
<div class="form-check notification-channels">
<ng-container *ngFor="let preference of preferences">
<label *ngIf="preference.visible" class="pref-channels">
<input
class="form-check-input check-box"
role="checkbox"
type="checkbox"
[checked]="preference.enabled"
(change)="updatePreference(preference)"
[disabled]="isLoading$ | async"
/>
<span class="check-label">
{{
'notificationPreference.' + preference.channel | cxTranslate
}}
{{ preference.value }}
</span>
</label>
</ng-container>
</div>
<label class="note"
><strong>{{ 'notificationPreference.note' | cxTranslate }}</strong
>{{ 'notificationPreference.noteMessage' | cxTranslate }}
</label>
</div>
</div>
</div>

<ng-template #loading>
<div class="cx-spinner">
<cx-spinner></cx-spinner>
</div>
</ng-template>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NewNotificationPreferenceComponent } from './new-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: ` <div>spinner</div> `,
})
class MockCxSpinnerComponent {}

describe('NewNotificationPreferenceComponent', () => {
let component: NewNotificationPreferenceComponent;
let fixture: ComponentFixture<NewNotificationPreferenceComponent>;
let el: DebugElement;

const newNotificationPreferenceService = jasmine.createSpyObj(
'UserNotificationPreferenceService',
[
'getPreferences',
'loadPreferences',
'getPreferencesLoading',
'updatePreferences',
'getUpdatePreferencesResultLoading',
'resetNotificationPreferences',
]
);

const newNotificationPreference: NotificationPreference[] = [
{
channel: 'EMAIL',
enabled: true,
value: '[email protected]',
visible: true,
},
{
channel: 'SMS',
enabled: false,
value: '01234567890',
visible: true,
},
];

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [I18nTestingModule],
declarations: [
NewNotificationPreferenceComponent,
MockCxSpinnerComponent,
],
providers: [
{
provide: UserNotificationPreferenceService,
useValue: newNotificationPreferenceService,
},
],
}).compileComponents();
})
);

beforeEach(() => {
fixture = TestBed.createComponent(NewNotificationPreferenceComponent);
el = fixture.debugElement;
component = fixture.componentInstance;

newNotificationPreferenceService.loadPreferences.and.stub();
newNotificationPreferenceService.updatePreferences.and.stub();
newNotificationPreferenceService.getPreferences.and.returnValue(
of(newNotificationPreference)
);
newNotificationPreferenceService.getPreferencesLoading.and.returnValue(
of(false)
);
newNotificationPreferenceService.getUpdatePreferencesResultLoading.and.returnValue(
of(false)
);
newNotificationPreferenceService.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 ===
newNotificationPreference.length
).toBeTruthy();
expect(
el.queryAll(By.css('.pref-channels')).length ===
newNotificationPreference.length
).toBeTruthy();
});

it('should show spinner when loading', () => {
newNotificationPreferenceService.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', () => {
newNotificationPreferenceService.getUpdatePreferencesResultLoading.and.returnValue(
of(false)
);
newNotificationPreferenceService.getPreferencesLoading.and.returnValue(
cold('-a|', { a: true })
);
fixture.detectChanges();

const cheboxies = el.queryAll(By.css('.form-check-input'));
expect(cheboxies.length).toEqual(newNotificationPreference.length);
const chx = cheboxies[0].nativeElement;
chx.click();

getTestScheduler().flush();
fixture.detectChanges();

expect(
newNotificationPreferenceService.updatePreferences
).toHaveBeenCalled();
expect(chx.disabled).toEqual(true);
});

it('should be able to disable a channel when update loading', () => {
newNotificationPreferenceService.getPreferencesLoading.and.returnValue(
of(false)
);
newNotificationPreferenceService.getUpdatePreferencesResultLoading.and.returnValue(
cold('-a|', { a: true })
);
fixture.detectChanges();

const cheboxies = el.queryAll(By.css('.form-check-input'));
expect(cheboxies.length).toEqual(newNotificationPreference.length);
const chx = cheboxies[0].nativeElement;
chx.click();

getTestScheduler().flush();
fixture.detectChanges();

expect(
newNotificationPreferenceService.updatePreferences
).toHaveBeenCalled();
expect(chx.disabled).toEqual(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import {
NotificationPreference,
UserNotificationPreferenceService,
} from '@spartacus/core';
import { combineLatest, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

@Component({
selector: 'cx-new-notification-preference',
templateUrl: './new-notification-preference.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NewNotificationPreferenceComponent implements OnInit {
preferences$: Observable<NotificationPreference[]>;
isLoading$: Observable<boolean>;

protected preferences: NotificationPreference[] = [];

constructor(
private notificationPreferenceService: UserNotificationPreferenceService
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use protected instead of private to make this easier for users to extend.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified as suggestion.Only after CDC/CDP changing cms config and introduce `newNotificationPreference' in the future, it will be displayed on SPA UI. That's why there is no e2e test. But we already have a ticket to track it.
https://jira.tools.sap/browse/CXSPA-4475

) {}

ngOnInit() {
this.notificationPreferenceService.resetNotificationPreferences();
this.preferences$ = this.notificationPreferenceService
.getPreferences()
.pipe(tap((preferences) => (this.preferences = preferences)));
this.notificationPreferenceService.loadPreferences();

this.isLoading$ = combineLatest([
this.notificationPreferenceService.getPreferencesLoading(),
this.notificationPreferenceService.getUpdatePreferencesResultLoading(),
]).pipe(
map(([prefsLoading, updateLoading]) => prefsLoading || updateLoading)
);
}

updatePreference(preference: NotificationPreference) {
const updatedPreferences: NotificationPreference[] = [];
this.preferences.forEach((p) => {
if (p.channel === preference.channel) {
updatedPreferences.push({
...p,
enabled: !p.enabled,
});
} else {
updatedPreferences.push(p);
}
});
this.notificationPreferenceService.updatePreferences(updatedPreferences);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2023 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import {
AuthGuard,
I18nModule,
CmsConfig,
provideDefaultConfig,
} 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 { NewNotificationPreferenceComponent } from './new-notification-preference.component';

@NgModule({
declarations: [NewNotificationPreferenceComponent],
imports: [
CommonModule,
SpinnerModule,
I18nModule,
RouterModule.forChild([
{
// @ts-ignore
path: null,
Zeyber marked this conversation as resolved.
Show resolved Hide resolved
canActivate: [AuthGuard, CmsPageGuard],
component: PageLayoutComponent,
data: { cxRoute: 'notificationPreference' },
},
]),
],
providers: [
provideDefaultConfig(<CmsConfig>{
cmsComponents: {
NewNotificationPreferenceComponent: {
component: NewNotificationPreferenceComponent,
guards: [AuthGuard],
},
},
}),
],
exports: [NewNotificationPreferenceComponent],
})
export class NewNotificationPreferenceModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
@import './my-coupons-card';
@import './my-coupons-dialog';
@import './my-interests';
@import './new-notification-preference';

$myaccount-components-allowlist: cx-address-book, cx-address-form,
cx-suggested-addresses-dialog, 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-consent-management-form, cx-consent-management,
cx-new-notification-preference, cx-my-interests, cx-my-coupons, cx-coupon-card,
cx-coupon-dialog, cx-payment-methods !default;
Loading
Loading