diff --git a/docs/README.md b/docs/README.md index 03b5ddd155..46cabc8b0b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -88,3 +88,4 @@ kb_sync_latest_only - [Guide - Monitoring with Prometheus](./guides/prometheus-monitoring.md) - [Guide - Store Locator with Google Maps](./guides/store-locator.md) - [Guide - Address Check with Address Doctor](./guides/address-doctor.md) +- [Guide - E-Mail Marketing/Newsletter Subscription](./guides/newsletter-subscription.md) diff --git a/docs/guides/newsletter-subscription.md b/docs/guides/newsletter-subscription.md new file mode 100644 index 0000000000..3dce84e38b --- /dev/null +++ b/docs/guides/newsletter-subscription.md @@ -0,0 +1,26 @@ + + +# E-Mail Marketing/Newsletter Subscription + +## Introduction + +In the PWA registered users can subscribe to and unsubscribe from a newsletter service. + +## Configuration + +To enable this feature, an e-mail marketing provider has to be configured in Intershop Commerce Management under **Channel Preferences** | **E-mail Marketing**. +The PWA receives information about the e-mail provider configuration via the `/configurations` REST call under `marketing.newsletterSubscriptionEnabled`. + +## Storefront + +If the newsletter subscription feature is enabled, an additional checkbox is displayed on the registration page and on the account profile page that enables the user to subscribe to or unsubscribe from the newsletter service. + +## Further References + +- [Intershop Knowledge Base | Concept - E-Mail Marketing / Newsletter Subscription](https://support.intershop.com/kb/index.php/Display/2G9985) +- [Intershop Knowledge Base | Cookbook - E-Mail Marketing / Newsletter Subscription](https://support.intershop.com/kb/index.php/Display/30973Y) diff --git a/e2e/cypress/e2e/specs/account/login-user.b2c.e2e-spec.ts b/e2e/cypress/e2e/specs/account/login-user.b2c.e2e-spec.ts index e159f79e30..cb60e8db6c 100644 --- a/e2e/cypress/e2e/specs/account/login-user.b2c.e2e-spec.ts +++ b/e2e/cypress/e2e/specs/account/login-user.b2c.e2e-spec.ts @@ -1,18 +1,23 @@ import { at } from '../../framework'; +import { createUserViaREST } from '../../framework/users'; import { LoginPage } from '../../pages/account/login.page'; import { MyAccountPage } from '../../pages/account/my-account.page'; +import { sensibleDefaults } from '../../pages/account/registration.page'; import { HomePage } from '../../pages/home.page'; const _ = { - name: 'Patricia Miller', - email: 'patricia@test.intershop.de', + user: { ...sensibleDefaults, login: `testuser${new Date().getTime()}@test.intershop.de` }, password: '!InterShop00!', wrongPassword: 'wrong', }; describe('Returning User', () => { describe('with valid password', () => { - before(() => HomePage.navigateTo()); + before(() => { + createUserViaREST(_.user); + + HomePage.navigateTo(); + }); it('should press login and be routed to login page', () => { at(HomePage, page => { @@ -24,11 +29,11 @@ describe('Returning User', () => { it('should enter credentials and submit and be directed to my-account', () => { at(LoginPage, page => { page.errorText.should('not.exist'); - page.fillForm(_.email, _.password); + page.fillForm(_.user.login, _.password); page.submit().its('response.statusCode').should('equal', 200); }); at(MyAccountPage, page => { - page.header.myAccountLink.should('have.text', _.name); + page.header.myAccountLink.should('have.text', `${_.user.firstName} ${_.user.lastName}`); }); }); @@ -66,7 +71,7 @@ describe('Returning User', () => { it('should enter wrong credentials and submit and be be still at login page', () => { at(LoginPage, page => { - page.fillForm(_.email, _.wrongPassword); + page.fillForm(_.user.login, _.wrongPassword); page.submit().its('response.statusCode').should('equal', 401); page.errorText.should('be.visible'); }); diff --git a/src/app/core/facades/account.facade.ts b/src/app/core/facades/account.facade.ts index 1b8fac8276..3c2481cf36 100644 --- a/src/app/core/facades/account.facade.ts +++ b/src/app/core/facades/account.facade.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Store, select } from '@ngrx/store'; -import { Subject } from 'rxjs'; +import { Observable, Subject, of } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; import { Address } from 'ish-core/models/address/address.model'; @@ -13,6 +13,7 @@ import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-in import { User } from 'ish-core/models/user/user.model'; import { OrderListQuery } from 'ish-core/services/order/order.service'; import { MessagesPayloadType } from 'ish-core/store/core/messages'; +import { getServerConfigParameter } from 'ish-core/store/core/server-config'; import { createCustomerAddress, deleteCustomerAddress, @@ -48,6 +49,7 @@ import { getCustomerApprovalEmail, getLoggedInCustomer, getLoggedInUser, + getNewsletterSubscriptionStatus, getPasswordReminderError, getPasswordReminderSuccess, getPriceDisplayType, @@ -68,6 +70,7 @@ import { updateUserPassword, updateUserPasswordByPasswordReminder, updateUserPreferredPayment, + userNewsletterActions, } from 'ish-core/store/customer/user'; import { whenTruthy } from 'ish-core/utils/operators'; @@ -259,6 +262,30 @@ export class AccountFacade { this.store.dispatch(updateCustomerAddress({ address })); } + // NEWSLETTER + subscribedToNewsletter$ = this.store.pipe(select(getNewsletterSubscriptionStatus)); + + loadNewsletterSubscription(): Observable { + return this.store.pipe( + select(getServerConfigParameter('marketing.newsletterSubscriptionEnabled')), + take(1), + switchMap(enabled => { + if (enabled) { + this.store.dispatch(userNewsletterActions.loadUserNewsletterSubscription()); + return this.store.pipe(select(getNewsletterSubscriptionStatus)); + } + return of(false); + }) + ); + } + + // should only be called when the server-configuration-parameter 'marketing.newsletterSubscriptionEnabled' is true + updateNewsletterSubscription(subscribedToNewsletter: boolean) { + this.store.dispatch( + userNewsletterActions.updateUserNewsletterSubscription({ subscriptionStatus: subscribedToNewsletter }) + ); + } + // DATA REQUESTS dataRequestLoading$ = this.store.pipe(select(getDataRequestLoading)); diff --git a/src/app/core/models/customer/customer.model.ts b/src/app/core/models/customer/customer.model.ts index f425cc722c..610d358d3b 100644 --- a/src/app/core/models/customer/customer.model.ts +++ b/src/app/core/models/customer/customer.model.ts @@ -33,8 +33,9 @@ export type CustomerUserType = { * registration request data type */ export type CustomerRegistrationType = { - credentials?: Credentials; address: Address; + credentials?: Credentials; + subscribedToNewsletter?: boolean; } & CustomerUserType & Captcha; @@ -42,4 +43,5 @@ export interface SsoRegistrationType { companyInfo: { companyName1: string; companyName2?: string; taxationID: string }; address: Address; userId: string; + subscribedToNewsletter?: boolean; } diff --git a/src/app/core/services/newsletter/newsletter.service.spec.ts b/src/app/core/services/newsletter/newsletter.service.spec.ts new file mode 100644 index 0000000000..b4130d6640 --- /dev/null +++ b/src/app/core/services/newsletter/newsletter.service.spec.ts @@ -0,0 +1,107 @@ +import { TestBed } from '@angular/core/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { of, throwError } from 'rxjs'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; + +import { ApiService } from 'ish-core/services/api/api.service'; +import { getNewsletterSubscriptionStatus } from 'ish-core/store/customer/user'; +import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; + +import { NewsletterService } from './newsletter.service'; + +describe('Newsletter Service', () => { + let newsletterService: NewsletterService; + let apiServiceMock: ApiService; + let store$: MockStore; + + let userEmail: string; + + beforeEach(() => { + apiServiceMock = mock(ApiService); + TestBed.configureTestingModule({ + providers: [{ provide: ApiService, useFactory: () => instance(apiServiceMock) }, provideMockStore()], + }); + newsletterService = TestBed.inject(NewsletterService); + store$ = TestBed.inject(MockStore); + + userEmail = 'user@test.com'; + + store$.overrideSelector(getNewsletterSubscriptionStatus, false); + }); + + it("should subscribe user to newsletter when 'updateNewsletterSubscriptionStatus' is called with 'true'", done => { + when(apiServiceMock.post(anything(), anything())).thenReturn(of(true)); + + const newStatus = true; + + newsletterService.updateNewsletterSubscriptionStatus(newStatus, userEmail).subscribe(subscriptionStatus => { + verify(apiServiceMock.post(`subscriptions`, anything())).once(); + expect(subscriptionStatus).toBeTrue(); + done(); + }); + }); + + it("should unsubscribe user from the newsletter when 'updateNewsletterSubscriptionStatus' is called with 'false'", done => { + when(apiServiceMock.delete(anything())).thenReturn(of(false)); + store$.overrideSelector(getNewsletterSubscriptionStatus, true); + + const newStatus = false; + + newsletterService.updateNewsletterSubscriptionStatus(newStatus, userEmail).subscribe(subscriptionStatus => { + verify(apiServiceMock.delete(`subscriptions/${userEmail}`)).once(); + expect(subscriptionStatus).toBeFalse(); + done(); + }); + }); + + it("should not make an API call when calling 'updateNewsletterSubscriptionStatus' and the status hasn't changed", done => { + when(apiServiceMock.delete(anything())).thenReturn(of(false)); + store$.overrideSelector(getNewsletterSubscriptionStatus, true); + + const newStatus = true; + + newsletterService.updateNewsletterSubscriptionStatus(newStatus, userEmail).subscribe(subscriptionStatus => { + verify(apiServiceMock.delete(`subscriptions/${userEmail}`)).never(); + expect(subscriptionStatus).toBeTrue(); + done(); + }); + }); + + it("should get the users subscription-status when 'getSubscription' is called", done => { + when(apiServiceMock.get(anything())).thenReturn(of({ active: true })); + + newsletterService.getSubscription(userEmail).subscribe(subscriptionStatus => { + verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once(); + expect(subscriptionStatus).toBeTrue(); + done(); + }); + + when(apiServiceMock.get(anything())).thenReturn(of({ active: false })); + + newsletterService.getSubscription(userEmail).subscribe(subscriptionStatus => { + verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once(); + expect(subscriptionStatus).toBeFalse(); + done(); + }); + }); + + it('should return false when "getSubscription" is called and a 404-error is thrown', done => { + when(apiServiceMock.get(anything())).thenReturn( + throwError(() => makeHttpError({ message: 'No subscription found', status: 404 })) + ); + + newsletterService.getSubscription(userEmail).subscribe(subscriptionStatus => { + verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once(); + expect(subscriptionStatus).toBeFalse(); + done(); + }); + + when(apiServiceMock.get(anything())).thenReturn(of({ active: false })); + + newsletterService.getSubscription(userEmail).subscribe(subscriptionStatus => { + verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once(); + expect(subscriptionStatus).toBeFalse(); + done(); + }); + }); +}); diff --git a/src/app/core/services/newsletter/newsletter.service.ts b/src/app/core/services/newsletter/newsletter.service.ts new file mode 100644 index 0000000000..6134e3316e --- /dev/null +++ b/src/app/core/services/newsletter/newsletter.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { Store, select } from '@ngrx/store'; +import { Observable, catchError, map, of, switchMap, take, throwError } from 'rxjs'; + +import { ApiService } from 'ish-core/services/api/api.service'; +import { getNewsletterSubscriptionStatus } from 'ish-core/store/customer/user'; + +/** + * The Newsletter Service handles the newsletter related interaction with the 'subscriptions' REST API. + */ +@Injectable({ providedIn: 'root' }) +export class NewsletterService { + constructor(private apiService: ApiService, private store: Store) {} + + private newsletterSubscriptionStatus$ = this.store.pipe(select(getNewsletterSubscriptionStatus), take(1)); + + /** + * Gets the current newsletter subscription status of the user. + * + * @param userEmail The user email. + * @returns The current newsletter subscription status. + * Returns 'false' when a 404-error is thrown, which is the APIs response for "no subscription found". + */ + getSubscription(userEmail: string): Observable { + return this.apiService.get(`subscriptions/${userEmail}`).pipe( + map((params: { active: boolean }) => params.active), + catchError(error => { + if (error.status === 404) { + return of(false); + } + return throwError(() => error); + }) + ); + } + + /** + * Updates the newsletter subscription status of the user. + * Doesn't make a REST call when newStatus and currentStatus are the same. + * + * @param newStatus The new newsletter subscription status of the user. + * @param userEmail The user e-mail. + * @returns The new newsletter subscription status. + * Returns the current status when newStatus and currentStatus are the same. + */ + updateNewsletterSubscriptionStatus(newStatus: boolean, userEmail: string): Observable { + // only make a REST-call when the status has changed + return this.newsletterSubscriptionStatus$.pipe( + switchMap(currentStatus => { + if (currentStatus === newStatus) { + return of(currentStatus); + } + + return newStatus ? this.subscribeToNewsletter(userEmail) : this.unsubscribeFromNewsletter(userEmail); + }) + ); + } + + /** + * always returns 'true' + */ + private subscribeToNewsletter(userEmail: string): Observable { + const requestBody = { + name: 'Newsletter', + type: 'Subscription', + active: true, + recipient: userEmail, + }; + + return this.apiService.post(`subscriptions`, requestBody).pipe(map(() => true)); + } + + /** + * always returns 'false' + */ + private unsubscribeFromNewsletter(userEmail: string): Observable { + return this.apiService.delete(`subscriptions/${userEmail}`).pipe(map(() => false)); + } +} diff --git a/src/app/core/store/customer/customer-store.module.ts b/src/app/core/store/customer/customer-store.module.ts index 0626b91f06..f46ac2df8c 100644 --- a/src/app/core/store/customer/customer-store.module.ts +++ b/src/app/core/store/customer/customer-store.module.ts @@ -25,6 +25,7 @@ import { OrganizationManagementEffects } from './organization-management/organiz import { RequisitionManagementEffects } from './requisition-management/requisition-management.effects'; import { SsoRegistrationEffects } from './sso-registration/sso-registration.effects'; import { ssoRegistrationReducer } from './sso-registration/sso-registration.reducer'; +import { UserNewsletterEffects } from './user/user-newsletter.effects'; import { UserEffects } from './user/user.effects'; import { userReducer } from './user/user.reducer'; @@ -53,6 +54,7 @@ const customerEffects = [ RequisitionManagementEffects, SsoRegistrationEffects, DataRequestsEffects, + UserNewsletterEffects, ]; @Injectable() diff --git a/src/app/core/store/customer/customer-store.spec.ts b/src/app/core/store/customer/customer-store.spec.ts index 9dddc1cd50..24da13d989 100644 --- a/src/app/core/store/customer/customer-store.spec.ts +++ b/src/app/core/store/customer/customer-store.spec.ts @@ -21,6 +21,7 @@ import { ConfigurationService } from 'ish-core/services/configuration/configurat import { CountryService } from 'ish-core/services/country/country.service'; import { DataRequestsService } from 'ish-core/services/data-requests/data-requests.service'; import { FilterService } from 'ish-core/services/filter/filter.service'; +import { NewsletterService } from 'ish-core/services/newsletter/newsletter.service'; import { OrderService } from 'ish-core/services/order/order.service'; import { PaymentService } from 'ish-core/services/payment/payment.service'; import { PricesService } from 'ish-core/services/prices/prices.service'; @@ -178,6 +179,7 @@ describe('Customer Store', () => { { provide: CookiesService, useFactory: () => instance(mock(CookiesService)) }, { provide: DataRequestsService, useFactory: () => instance(mock(DataRequestsService)) }, { provide: FilterService, useFactory: () => instance(mock(FilterService)) }, + { provide: NewsletterService, useFactory: () => instance(mock(NewsletterService)) }, { provide: OrderService, useFactory: () => instance(mock(OrderService)) }, { provide: PaymentService, useFactory: () => instance(mock(PaymentService)) }, { provide: PricesService, useFactory: () => instance(productPriceServiceMock) }, diff --git a/src/app/core/store/customer/sso-registration/sso-registration.effects.ts b/src/app/core/store/customer/sso-registration/sso-registration.effects.ts index 36f3f311d8..13042c4af9 100644 --- a/src/app/core/store/customer/sso-registration/sso-registration.effects.ts +++ b/src/app/core/store/customer/sso-registration/sso-registration.effects.ts @@ -1,11 +1,12 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { map, mergeMap } from 'rxjs/operators'; +import { concatMap, mergeMap } from 'rxjs/operators'; import { v4 as uuid } from 'uuid'; import { FeatureToggleService } from 'ish-core/feature-toggle.module'; import { SsoRegistrationType } from 'ish-core/models/customer/customer.model'; import { UserService } from 'ish-core/services/user/user.service'; +import { userNewsletterActions } from 'ish-core/store/customer/user'; import { mapErrorToAction, mapToPayload } from 'ish-core/utils/operators'; import { registerFailure, registerSuccess, setRegistrationInfo } from './sso-registration.actions'; @@ -35,7 +36,20 @@ export class SsoRegistrationEffects { }, userId: data.userId, }) - .pipe(map(registerSuccess), mapErrorToAction(registerFailure)) + .pipe( + concatMap(createUserResponse => [ + registerSuccess, + ...(data.subscribedToNewsletter + ? [ + userNewsletterActions.updateUserNewsletterSubscription({ + subscriptionStatus: true, + userEmail: createUserResponse.user.email, + }), + ] + : []), + ]), + mapErrorToAction(registerFailure) + ) ) ) ); diff --git a/src/app/core/store/customer/user/user-newsletter.effects.spec.ts b/src/app/core/store/customer/user/user-newsletter.effects.spec.ts new file mode 100644 index 0000000000..9403156844 --- /dev/null +++ b/src/app/core/store/customer/user/user-newsletter.effects.spec.ts @@ -0,0 +1,121 @@ +import { TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Action } from '@ngrx/store'; +import { provideMockStore } from '@ngrx/store/testing'; +import { cold, hot } from 'jasmine-marbles'; +import { Observable, of } from 'rxjs'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; + +import { NewsletterService } from 'ish-core/services/newsletter/newsletter.service'; +import { getLoggedInUser } from 'ish-core/store/customer/user'; + +import { UserNewsletterEffects } from './user-newsletter.effects'; +import { userNewsletterActions, userNewsletterApiActions } from './user.actions'; + +describe('User Newsletter Effects', () => { + let actions$: Observable; + let effects: UserNewsletterEffects; + let newsletterServiceMock: NewsletterService; + + const testEmail = 'test@intershop.com'; + + beforeEach(() => { + newsletterServiceMock = mock(NewsletterService); + + when(newsletterServiceMock.getSubscription(anything())).thenReturn(of(true)); + when(newsletterServiceMock.updateNewsletterSubscriptionStatus(anything(), anything())).thenReturn(of(true)); + + TestBed.configureTestingModule({ + providers: [ + { provide: NewsletterService, useFactory: () => instance(newsletterServiceMock) }, + provideMockActions(() => actions$), + provideMockStore({ + selectors: [{ selector: getLoggedInUser, value: { email: testEmail } }], + }), + UserNewsletterEffects, + ], + }); + + effects = TestBed.inject(UserNewsletterEffects); + }); + + describe('loadUserNewsletterSubscription$', () => { + it('should call the newsletter service when LoadUserNewsletterSubscription event is called', done => { + const action = userNewsletterActions.loadUserNewsletterSubscription(); + + actions$ = of(action); + + effects.loadUserNewsletterSubscription$.subscribe(() => { + verify(newsletterServiceMock.getSubscription(testEmail)).once(); + done(); + }); + }); + + it('should map to action of type LoadUserNewsletterSubscriptionSuccess', () => { + const action = userNewsletterActions.loadUserNewsletterSubscription(); + const completion = userNewsletterApiActions.loadUserNewsletterSubscriptionSuccess({ subscribed: true }); + + actions$ = hot('-a', { a: action }); + const expected$ = cold('-c', { c: completion }); + + expect(effects.loadUserNewsletterSubscription$).toBeObservable(expected$); + }); + }); + + describe('updateUserNewsletterSubscription$', () => { + it('should call the newsletter service when UpdateUserNewsletterSubscription event is called', done => { + const action = userNewsletterActions.updateUserNewsletterSubscription({ + subscriptionStatus: true, + }); + + actions$ = of(action); + + effects.updateNewsletterSubscription$.subscribe(() => { + verify(newsletterServiceMock.updateNewsletterSubscriptionStatus(true, testEmail)).once(); + done(); + }); + }); + + it('should use the email of the logged-in user by default when no email is provided', done => { + const action = userNewsletterActions.updateUserNewsletterSubscription({ + subscriptionStatus: true, + userEmail: undefined, + }); + + actions$ = of(action); + + effects.updateNewsletterSubscription$.subscribe(() => { + verify(newsletterServiceMock.updateNewsletterSubscriptionStatus(true, testEmail)).once(); + done(); + }); + }); + + it('should prioritize the provided email over the logged-in user email', done => { + const newTestEmail = 'newTestMail@intershop.com'; + + const action = userNewsletterActions.updateUserNewsletterSubscription({ + subscriptionStatus: true, + userEmail: newTestEmail, + }); + + actions$ = of(action); + + effects.updateNewsletterSubscription$.subscribe(() => { + verify(newsletterServiceMock.updateNewsletterSubscriptionStatus(true, newTestEmail)).once(); + done(); + }); + }); + + it('should map to action of type UpdateUserNewsletterSubscriptionSuccess', () => { + const action = userNewsletterActions.updateUserNewsletterSubscription({ + subscriptionStatus: true, + }); + const completion = userNewsletterApiActions.updateUserNewsletterSubscriptionSuccess({ subscriptionStatus: true }); + + actions$ = hot('-a', { a: action }); + const expected$ = cold('-c', { c: completion }); + + expect(effects.updateNewsletterSubscription$).toBeObservable(expected$); + }); + }); +}); diff --git a/src/app/core/store/customer/user/user-newsletter.effects.ts b/src/app/core/store/customer/user/user-newsletter.effects.ts new file mode 100644 index 0000000000..7f3e57126d --- /dev/null +++ b/src/app/core/store/customer/user/user-newsletter.effects.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; +import { Store, select } from '@ngrx/store'; +import { concatMap, filter, map, take } from 'rxjs/operators'; + +import { NewsletterService } from 'ish-core/services/newsletter/newsletter.service'; +import { mapErrorToAction, mapToPayload, whenTruthy } from 'ish-core/utils/operators'; + +import { userNewsletterActions, userNewsletterApiActions } from './user.actions'; +import { getLoggedInUser } from './user.selectors'; + +@Injectable() +export class UserNewsletterEffects { + constructor(private actions$: Actions, private store: Store, private newsletterService: NewsletterService) {} + + /** + * The effect has to wait for the getLoggedInUser-selector because it is used in a page-guard that is called before + * the user is ready in the store + */ + loadUserNewsletterSubscription$ = createEffect(() => + this.actions$.pipe( + ofType(userNewsletterActions.loadUserNewsletterSubscription), + concatMap(() => + this.store.pipe( + select(getLoggedInUser), + whenTruthy(), + take(1), + concatMap(user => + this.newsletterService.getSubscription(user.email).pipe( + map(subscriptionStatus => + userNewsletterApiActions.loadUserNewsletterSubscriptionSuccess({ subscribed: subscriptionStatus }) + ), + mapErrorToAction(userNewsletterApiActions.loadUserNewsletterSubscriptionFail) + ) + ) + ) + ) + ) + ); + + /** + * The user-email has to be provided when setting the subscription during the registration process + * because the user is not logged in yet. + * If no user-email is passed in, the email of the logged-in user is used. + */ + updateNewsletterSubscription$ = createEffect(() => + this.actions$.pipe( + ofType(userNewsletterActions.updateUserNewsletterSubscription), + mapToPayload(), + concatLatestFrom(() => this.store.pipe(select(getLoggedInUser))), + filter(([payload, user]) => !!payload.userEmail || !!user?.email), + concatMap(([payload, user]) => + this.newsletterService + .updateNewsletterSubscriptionStatus(payload.subscriptionStatus, payload.userEmail || user.email) + .pipe( + map(subscriptionStatus => + userNewsletterApiActions.updateUserNewsletterSubscriptionSuccess({ subscriptionStatus }) + ), + mapErrorToAction(userNewsletterApiActions.updateUserNewsletterSubscriptionFail) + ) + ) + ) + ); +} diff --git a/src/app/core/store/customer/user/user.actions.ts b/src/app/core/store/customer/user/user.actions.ts index fa203d0512..87bd11defc 100644 --- a/src/app/core/store/customer/user/user.actions.ts +++ b/src/app/core/store/customer/user/user.actions.ts @@ -1,4 +1,4 @@ -import { createAction } from '@ngrx/store'; +import { createAction, createActionGroup, emptyProps } from '@ngrx/store'; import { Credentials } from 'ish-core/models/credentials/credentials.model'; import { Customer, CustomerLoginType, CustomerRegistrationType } from 'ish-core/models/customer/customer.model'; @@ -146,3 +146,21 @@ export const updateUserPasswordByPasswordReminderFail = createAction( ); export const fetchAnonymousUserToken = createAction('[Token API] Fetch Anonymous User Token'); + +export const userNewsletterActions = createActionGroup({ + source: 'User Newsletter', + events: { + 'Load User Newsletter Subscription': emptyProps(), + 'Update User Newsletter Subscription': payload<{ subscriptionStatus: boolean; userEmail?: string }>(), + }, +}); + +export const userNewsletterApiActions = createActionGroup({ + source: 'User Newsletter API', + events: { + 'Load User Newsletter Subscription Success': payload<{ subscribed: boolean }>(), + 'Load User Newsletter Subscription Fail': httpError<{}>(), + 'Update User Newsletter Subscription Success': payload<{ subscriptionStatus: boolean }>(), + 'Update User Newsletter Subscription Fail': httpError<{}>(), + }, +}); diff --git a/src/app/core/store/customer/user/user.effects.spec.ts b/src/app/core/store/customer/user/user.effects.spec.ts index bacd3d2ccf..0e3d108d07 100644 --- a/src/app/core/store/customer/user/user.effects.spec.ts +++ b/src/app/core/store/customer/user/user.effects.spec.ts @@ -63,6 +63,7 @@ import { updateUserPasswordSuccess, updateUserPreferredPayment, updateUserSuccess, + userNewsletterActions, } from './user.actions'; import { UserEffects } from './user.effects'; @@ -416,6 +417,26 @@ describe('User Effects', () => { expect(effects.createUser$).toBeObservable(expected$); }); + it('should dispatch a subscribeUserToNewsletter action when the newsletter-subscription is checked', () => { + const credentials: Credentials = { login: '1234', password: 'xxx' }; + const customer: Customer = { isBusinessCustomer: false, customerNo: 'PC' }; + + when(userServiceMock.createUser(anything())).thenReturn(of(customerLoginType)); + + const action = createUser({ customer, credentials, subscribedToNewsletter: true } as CustomerRegistrationType); + const completion1 = createUserSuccess({ email: customerLoginType.user.email }); + const completion2 = userNewsletterActions.updateUserNewsletterSubscription({ + subscriptionStatus: true, + userEmail: customerLoginType.user.email, + }); + const completion3 = loginUser({ credentials }); + + actions$ = hot('-a', { a: action }); + const expected$ = cold('-(bcd)', { b: completion1, c: completion2, d: completion3 }); + + expect(effects.createUser$).toBeObservable(expected$); + }); + it('should dispatch a CreateUserFail action on failed user creation', () => { const error = makeHttpError({ status: 401, code: 'field' }); when(userServiceMock.createUser(anything())).thenReturn(throwError(() => error)); diff --git a/src/app/core/store/customer/user/user.effects.ts b/src/app/core/store/customer/user/user.effects.ts index ff0266b2ae..dc9e2ae967 100644 --- a/src/app/core/store/customer/user/user.effects.ts +++ b/src/app/core/store/customer/user/user.effects.ts @@ -60,6 +60,7 @@ import { updateUserPreferredPayment, updateUserSuccess, userErrorReset, + userNewsletterActions, } from './user.actions'; import { getLoggedInCustomer, getLoggedInUser, getUserError } from './user.selectors'; @@ -153,6 +154,14 @@ export class UserEffects { ), concatMap(([createUserResponse, customerTypeForLoginApproval]) => [ createUserSuccess({ email: createUserResponse.user.email }), + ...(data.subscribedToNewsletter + ? [ + userNewsletterActions.updateUserNewsletterSubscription({ + subscriptionStatus: true, + userEmail: createUserResponse.user.email, + }), + ] + : []), customerTypeForLoginApproval?.includes(createUserResponse.customer.isBusinessCustomer ? 'SMB' : 'PRIVATE') ? createUserApprovalRequired({ email: createUserResponse.user.email }) : loginUser({ credentials: data.credentials }), diff --git a/src/app/core/store/customer/user/user.reducer.ts b/src/app/core/store/customer/user/user.reducer.ts index b8c405316b..c0836bd4cd 100644 --- a/src/app/core/store/customer/user/user.reducer.ts +++ b/src/app/core/store/customer/user/user.reducer.ts @@ -49,6 +49,8 @@ import { updateUserPasswordSuccess, updateUserSuccess, userErrorReset, + userNewsletterActions, + userNewsletterApiActions, } from './user.actions'; export interface UserState { @@ -63,6 +65,7 @@ export interface UserState { passwordReminderSuccess: boolean; passwordReminderError: HttpError; customerApprovalEmail: string; + subscribedToNewsletter: boolean; } const initialState: UserState = { @@ -77,6 +80,7 @@ const initialState: UserState = { passwordReminderSuccess: undefined, passwordReminderError: undefined, customerApprovalEmail: undefined, + subscribedToNewsletter: undefined, }; export const userReducer = createReducer( @@ -101,7 +105,9 @@ export const userReducer = createReducer( updateUserPasswordByPasswordReminder, requestPasswordReminder, loginUserWithToken, - logoutUser + logoutUser, + userNewsletterActions.loadUserNewsletterSubscription, + userNewsletterActions.updateUserNewsletterSubscription ), unsetLoadingOn( loadUserCostCentersFail, @@ -120,7 +126,9 @@ export const userReducer = createReducer( loadUserCostCentersSuccess, loadUserPaymentMethodsSuccess, deleteUserPaymentInstrumentSuccess, - logoutUserSuccess + logoutUserSuccess, + userNewsletterApiActions.loadUserNewsletterSubscriptionSuccess, + userNewsletterApiActions.updateUserNewsletterSubscriptionSuccess ), setErrorOn( updateUserFail, @@ -129,7 +137,9 @@ export const userReducer = createReducer( loadUserPaymentMethodsFail, deleteUserPaymentInstrumentFail, loadRolesAndPermissionsFail, - logoutUserFail + logoutUserFail, + userNewsletterApiActions.loadUserNewsletterSubscriptionFail, + userNewsletterApiActions.updateUserNewsletterSubscriptionFail ), on(loginUserFail, loadCompanyUserFail, createUserFail, (_, action): UserState => { const error = action.payload.error; @@ -212,6 +222,7 @@ export const userReducer = createReducer( ...state, }) ), + // Password Reminder on( updateUserPasswordByPasswordReminder, requestPasswordReminder, @@ -246,5 +257,20 @@ export const userReducer = createReducer( passwordReminderSuccess: undefined, passwordReminderError: undefined, }) + ), + // Newsletter + on( + userNewsletterApiActions.loadUserNewsletterSubscriptionSuccess, + (state, action): UserState => ({ + ...state, + subscribedToNewsletter: action.payload.subscribed, + }) + ), + on( + userNewsletterApiActions.updateUserNewsletterSubscriptionSuccess, + (state, action): UserState => ({ + ...state, + subscribedToNewsletter: action.payload.subscriptionStatus, + }) ) ); diff --git a/src/app/core/store/customer/user/user.selectors.spec.ts b/src/app/core/store/customer/user/user.selectors.spec.ts index 94c78347c1..27fc1699e8 100644 --- a/src/app/core/store/customer/user/user.selectors.spec.ts +++ b/src/app/core/store/customer/user/user.selectors.spec.ts @@ -25,11 +25,13 @@ import { requestPasswordReminderFail, requestPasswordReminderSuccess, updateUserPassword, + userNewsletterApiActions, } from './user.actions'; import { getCustomerApprovalEmail, getLoggedInCustomer, getLoggedInUser, + getNewsletterSubscriptionStatus, getPasswordReminderError, getPasswordReminderSuccess, getPriceDisplayType, @@ -285,4 +287,29 @@ describe('User Selectors', () => { expect(getCustomerApprovalEmail(store$.state)).toEqual(email); }); }); + + describe('getNewsletterSubscriptionStatus', () => { + beforeEach(() => { + store$.dispatch( + loginUserSuccess({ + customer: {} as Customer, + user: { email: 'test@interhop.com' } as User, + }) + ); + }); + + it('should be undefined if no action was triggered', () => { + expect(getNewsletterSubscriptionStatus(store$.state)).toBeUndefined(); + }); + + it('should return the current newsletter subscription status', () => { + store$.dispatch(userNewsletterApiActions.loadUserNewsletterSubscriptionSuccess({ subscribed: true })); + expect(getNewsletterSubscriptionStatus(store$.state)).toBeTrue(); + }); + + it('should update the newsletter subscription status', () => { + store$.dispatch(userNewsletterApiActions.updateUserNewsletterSubscriptionSuccess({ subscriptionStatus: false })); + expect(getNewsletterSubscriptionStatus(store$.state)).toBeFalse(); + }); + }); }); diff --git a/src/app/core/store/customer/user/user.selectors.ts b/src/app/core/store/customer/user/user.selectors.ts index 4b854f8cdc..61a593bf12 100644 --- a/src/app/core/store/customer/user/user.selectors.ts +++ b/src/app/core/store/customer/user/user.selectors.ts @@ -43,3 +43,5 @@ export const getPriceDisplayType = createSelector( return isB2B ? b2b || 'net' : b2c || 'gross'; } ); + +export const getNewsletterSubscriptionStatus = createSelector(getUserState, state => state.subscribedToNewsletter); diff --git a/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.ts b/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.ts index d6e95697d6..d0678ded71 100644 --- a/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.ts +++ b/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.ts @@ -41,6 +41,7 @@ export class AccountProfileEmailComponent implements OnInit { validation: [SpecialValidators.equalTo('emailConfirmation', 'email')], }, fieldGroup: [ + // New Email { key: 'email', type: 'ish-email-field', @@ -50,7 +51,7 @@ export class AccountProfileEmailComponent implements OnInit { required: true, }, }, - + // New Email confirmation { key: 'emailConfirmation', type: 'ish-text-input-field', @@ -67,6 +68,7 @@ export class AccountProfileEmailComponent implements OnInit { }, }, }, + // Password { key: 'currentPassword', type: 'ish-password-field', diff --git a/src/app/pages/account-profile-user/account-profile-user-page.component.html b/src/app/pages/account-profile-user/account-profile-user-page.component.html index ea1c175b2a..59cca60708 100644 --- a/src/app/pages/account-profile-user/account-profile-user-page.component.html +++ b/src/app/pages/account-profile-user/account-profile-user-page.component.html @@ -1,7 +1,9 @@ - + + - + + diff --git a/src/app/pages/account-profile-user/account-profile-user-page.component.spec.ts b/src/app/pages/account-profile-user/account-profile-user-page.component.spec.ts index d8d4481863..131743f7fa 100644 --- a/src/app/pages/account-profile-user/account-profile-user-page.component.spec.ts +++ b/src/app/pages/account-profile-user/account-profile-user-page.component.spec.ts @@ -1,11 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent } from 'ng-mocks'; import { instance, mock } from 'ts-mockito'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { AccountProfileUserPageComponent } from './account-profile-user-page.component'; -import { AccountProfileUserComponent } from './account-profile-user/account-profile-user.component'; describe('Account Profile User Page Component', () => { let component: AccountProfileUserPageComponent; @@ -14,7 +12,7 @@ describe('Account Profile User Page Component', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [AccountProfileUserPageComponent, MockComponent(AccountProfileUserComponent)], + declarations: [AccountProfileUserPageComponent], providers: [{ provide: AccountFacade, useFactory: () => instance(mock(AccountFacade)) }], }).compileComponents(); }); diff --git a/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.html b/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.html index 4c246ae1d7..a57004e2a6 100644 --- a/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.html +++ b/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.html @@ -4,24 +4,30 @@

{{ 'account.update_profile.heading' | translate }}

{{ 'account.update_profile.text' | translate }}

- -
-
-
-

- *{{ 'account.required_field.message' | translate }} -

-
- -
-
- - {{ 'account.cancel.link' | translate }} + +
+
+
+

+ *{{ 'account.required_field.message' | translate }} +

+ + +
+
+ + {{ 'account.cancel.link' | translate }} +
-
- + +
-
+ diff --git a/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.spec.ts b/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.spec.ts index 1e5f69b769..4a474fb9bf 100644 --- a/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.spec.ts +++ b/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.spec.ts @@ -1,8 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; import { anything, instance, mock, spy, verify, when } from 'ts-mockito'; +import { AccountFacade } from 'ish-core/facades/account.facade'; +import { AppFacade } from 'ish-core/facades/app.facade'; import { User } from 'ish-core/models/user/user.model'; import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component'; import { FormlyTestingModule } from 'ish-shared/formly/dev/testing/formly-testing.module'; @@ -14,11 +17,20 @@ describe('Account Profile User Component', () => { let component: AccountProfileUserComponent; let fixture: ComponentFixture; let element: HTMLElement; - let fieldLibrary: FieldLibrary; + let fieldLibraryMock: FieldLibrary; + + let appFacadeMock: AppFacade; + let accountFacadeMock: AccountFacade; + + let currentUser: User; + let invalidUser: User; beforeEach(async () => { - fieldLibrary = mock(FieldLibrary); - when(fieldLibrary.getConfigurationGroup(anything())).thenReturn([ + appFacadeMock = mock(AppFacade); + accountFacadeMock = mock(AccountFacade); + + fieldLibraryMock = mock(FieldLibrary); + when(fieldLibraryMock.getConfigurationGroup(anything())).thenReturn([ { key: 'title', type: 'ish-select-field', @@ -43,7 +55,11 @@ describe('Account Profile User Component', () => { await TestBed.configureTestingModule({ imports: [FormlyTestingModule, TranslateModule.forRoot()], declarations: [AccountProfileUserComponent, MockComponent(ErrorMessageComponent)], - providers: [{ provide: FieldLibrary, useFactory: () => instance(fieldLibrary) }], + providers: [ + { provide: AccountFacade, useFactory: () => instance(accountFacadeMock) }, + { provide: AppFacade, useFactory: () => instance(appFacadeMock) }, + { provide: FieldLibrary, useFactory: () => instance(fieldLibraryMock) }, + ], }).compileComponents(); }); @@ -51,6 +67,15 @@ describe('Account Profile User Component', () => { fixture = TestBed.createComponent(AccountProfileUserComponent); component = fixture.componentInstance; element = fixture.nativeElement; + + currentUser = { firstName: 'Patricia', lastName: 'Miller' } as User; + component.currentUser$ = of(currentUser); + + invalidUser = { firstName: '', lastName: '' } as User; + + when(appFacadeMock.serverSetting$(anything())).thenReturn(of(true)); + + when(accountFacadeMock.subscribedToNewsletter$).thenReturn(of(true)); }); it('should be created', () => { @@ -77,31 +102,43 @@ describe('Account Profile User Component', () => { expect(element.innerHTML).toContain('title'); }); + it('should display checkbox for newsletter subscription', () => { + fixture.detectChanges(); + + expect(element.innerHTML).toContain('newsletter'); + }); + it('should emit updateUserProfile event if form is valid', () => { const eventEmitter$ = spy(component.updateUserProfile); - component.currentUser = { firstName: 'Patricia', lastName: 'Miller' } as User; fixture.detectChanges(); - component.submit(); + component.submitForm(currentUser); verify(eventEmitter$.emit(anything())).once(); }); it('should not emit updateUserProfile event if form is invalid', () => { const eventEmitter$ = spy(component.updateUserProfile); + + component.currentUser$ = of(invalidUser); + fixture.detectChanges(); - component.submit(); + component.submitForm(anything()); verify(eventEmitter$.emit(anything())).never(); }); it('should disable submit button when the user submits an invalid form', () => { + component.currentUser$ = of(invalidUser); + fixture.detectChanges(); expect(component.buttonDisabled).toBeFalse(); - component.submit(); + + component.submitForm(anything()); + expect(component.buttonDisabled).toBeTrue(); }); }); diff --git a/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.ts b/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.ts index f9fc973c55..db89777163 100644 --- a/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.ts +++ b/src/app/pages/account-profile-user/account-profile-user/account-profile-user.component.ts @@ -2,7 +2,10 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output import { FormGroup } from '@angular/forms'; import { FormlyFieldConfig } from '@ngx-formly/core'; import { pick } from 'lodash-es'; +import { Observable, concatMap, map, of, withLatestFrom } from 'rxjs'; +import { AccountFacade } from 'ish-core/facades/account.facade'; +import { AppFacade } from 'ish-core/facades/app.facade'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; import { User } from 'ish-core/models/user/user.model'; import { FieldLibrary } from 'ish-shared/formly/field-library/field-library'; @@ -18,7 +21,7 @@ import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AccountProfileUserComponent implements OnInit { - @Input({ required: true }) currentUser: User; + @Input({ required: true }) currentUser$: Observable; @Input() error: HttpError; @Output() updateUserProfile = new EventEmitter(); @@ -26,32 +29,65 @@ export class AccountProfileUserComponent implements OnInit { submitted = false; accountProfileUserForm = new FormGroup({}); - model: Partial; - fields: FormlyFieldConfig[]; + model$: Observable>; + fields$: Observable; - constructor(private fieldLibrary: FieldLibrary) {} + constructor(private fieldLibrary: FieldLibrary, private accountFacade: AccountFacade, private appFacade: AppFacade) {} ngOnInit() { - this.model = pick(this.currentUser, 'title', 'firstName', 'lastName', 'phoneHome'); - this.fields = this.fieldLibrary.getConfigurationGroup('personalInfo'); + // only retrieves and sets the newsletter-subscription-status in the model if the server-setting for it is enabled + this.model$ = this.currentUser$.pipe( + map(user => pick(user, 'title', 'firstName', 'lastName', 'phoneHome')), + withLatestFrom(this.appFacade.serverSetting$('marketing.newsletterSubscriptionEnabled')), + concatMap(([userAttributes, newsletterSubscriptionEnabled]) => + this.accountFacade.subscribedToNewsletter$.pipe( + map(subscribedToNewsletter => + newsletterSubscriptionEnabled + ? { + ...userAttributes, + newsletter: subscribedToNewsletter, + } + : userAttributes + ) + ) + ) + ); + + // only displays the newsletter-subscription-field if the server-setting for the newsletter is enabled + this.fields$ = of(this.fieldLibrary.getConfigurationGroup('personalInfo')).pipe( + withLatestFrom(this.appFacade.serverSetting$('marketing.newsletterSubscriptionEnabled')), + map(([fields, newsletterSubscriptionEnabled]) => + newsletterSubscriptionEnabled ? [...fields, this.newsletterCheckboxField()] : fields + ) + ); + } + + newsletterCheckboxField(): FormlyFieldConfig { + return { + type: 'ish-checkbox-field', + key: 'newsletter', + props: { + label: 'registration.newsletter_subscription.text', + }, + }; } /** * Submits form and throws update event when form is valid */ - submit() { + submitForm(currentUser: User) { if (this.accountProfileUserForm.invalid) { this.submitted = true; markAsDirtyRecursive(this.accountProfileUserForm); return; } - const title = this.accountProfileUserForm.get('title').value; - const firstName = this.accountProfileUserForm.get('firstName').value; - const lastName = this.accountProfileUserForm.get('lastName').value; - const phoneHome = this.accountProfileUserForm.get('phoneHome').value; + if (this.accountProfileUserForm.get('newsletter')) { + const subscribedToNewsletter = this.accountProfileUserForm.get('newsletter').value; + this.accountFacade.updateNewsletterSubscription(subscribedToNewsletter); + } - this.updateUserProfile.emit({ ...this.currentUser, title, firstName, lastName, phoneHome }); + this.updateUserProfile.emit({ ...currentUser, ...this.accountProfileUserForm.value }); } get buttonDisabled() { diff --git a/src/app/pages/account-profile/account-profile-page.component.html b/src/app/pages/account-profile/account-profile-page.component.html index 30e6dd4584..66c1d46c49 100644 --- a/src/app/pages/account-profile/account-profile-page.component.html +++ b/src/app/pages/account-profile/account-profile-page.component.html @@ -1 +1,6 @@ - + diff --git a/src/app/pages/account-profile/account-profile-page.component.ts b/src/app/pages/account-profile/account-profile-page.component.ts index 788bedee1b..94c6621651 100644 --- a/src/app/pages/account-profile/account-profile-page.component.ts +++ b/src/app/pages/account-profile/account-profile-page.component.ts @@ -12,11 +12,14 @@ import { User } from 'ish-core/models/user/user.model'; export class AccountProfilePageComponent implements OnInit { user$: Observable; customer$: Observable; + subscribedToNewsletter$: Observable; constructor(private accountFacade: AccountFacade) {} ngOnInit() { this.user$ = this.accountFacade.user$; this.customer$ = this.accountFacade.customer$; + + this.subscribedToNewsletter$ = this.accountFacade.subscribedToNewsletter$; } } diff --git a/src/app/pages/account-profile/account-profile-page.module.ts b/src/app/pages/account-profile/account-profile-page.module.ts index b50d484cc2..b8c0e20c96 100644 --- a/src/app/pages/account-profile/account-profile-page.module.ts +++ b/src/app/pages/account-profile/account-profile-page.module.ts @@ -6,9 +6,10 @@ import { SharedModule } from 'ish-shared/shared.module'; import { AccountProfilePageComponent } from './account-profile-page.component'; import { AccountProfileComponent } from './account-profile/account-profile.component'; +import { fetchUserNewsletterGuard } from './fetch-user-newsletter.guard'; const routes: Routes = [ - { path: '', component: AccountProfilePageComponent }, + { path: '', canActivate: [fetchUserNewsletterGuard], component: AccountProfilePageComponent }, { path: 'email', data: { @@ -35,6 +36,7 @@ const routes: Routes = [ }, { path: 'user', + canActivate: [fetchUserNewsletterGuard], data: { breadcrumbData: [ { key: 'account.profile.link', link: '/account/profile' }, diff --git a/src/app/pages/account-profile/account-profile/account-profile.component.html b/src/app/pages/account-profile/account-profile/account-profile.component.html index 8bdf002ac0..75ccf70f1e 100644 --- a/src/app/pages/account-profile/account-profile/account-profile.component.html +++ b/src/app/pages/account-profile/account-profile/account-profile.component.html @@ -50,6 +50,7 @@

{{ 'account.profile.password.heading' | translate }}

+
@@ -57,14 +58,17 @@

{{ 'account.company_profile.detail.heading' | translate }}

+
{{ 'account.company_profile.name.label' | translate }}
{{ customer.companyName }}
{{ customer.companyName2 }}
+
{{ 'account.company_profile.industry.label' | translate }}
{{ customer.industry }}
+
{{ 'account.company_profile.taxation_id.label' | translate }}
{{ customer.taxationID }}
@@ -91,6 +95,7 @@

{{ 'account.profile.detail.heading' | translate }}

+
{{ 'account.profile.name.label' | translate }}
{{ user.title ? user.title + ' ' : '' }}{{ user.firstName }} {{ user.lastName }} @@ -98,7 +103,15 @@

{{ 'account.profile.detail.heading' | translate }}

{{ 'account.profile.phone.label' | translate }}
{{ user.phoneHome }}
- + + +
{{ 'account.profile.newsletter.label' | translate }}
+
+ + {{ (subscribedToNewsletter ? 'subscriptions.subscribed' : 'subscriptions.not_subscribed') | translate }} + +
+