Skip to content

Commit

Permalink
feat: subscription status on profile settings page
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasHengelhaupt committed Jan 26, 2024
1 parent 1a3aef0 commit 13d2b63
Show file tree
Hide file tree
Showing 22 changed files with 280 additions and 109 deletions.
12 changes: 12 additions & 0 deletions src/app/core/facades/account.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
getCustomerApprovalEmail,
getLoggedInCustomer,
getLoggedInUser,
getNewsletterSubscriptionStatus,
getPasswordReminderError,
getPasswordReminderSuccess,
getPriceDisplayType,
Expand All @@ -61,6 +62,7 @@ import {
updateUserPassword,
updateUserPasswordByPasswordReminder,
updateUserPreferredPayment,
userNewsletterActions,
} from 'ish-core/store/customer/user';
import { whenTruthy } from 'ish-core/utils/operators';

Expand Down Expand Up @@ -251,6 +253,16 @@ export class AccountFacade {
this.store.dispatch(updateCustomerAddress({ address }));
}

// NEWSLETTER

subscribedToNewsletter$ = this.store.pipe(select(getNewsletterSubscriptionStatus));

updateNewsletterSubscription(subscribedToNewsletter: boolean) {
this.store.dispatch(
userNewsletterActions.updateUserNewsletterStatus({ subscriptionStatus: subscribedToNewsletter })
);
}

// DATA REQUESTS

dataRequestLoading$ = this.store.pipe(select(getDataRequestLoading));
Expand Down
76 changes: 61 additions & 15 deletions src/app/core/services/newsletter/newsletter.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { of } from 'rxjs';
import { anyString, anything, instance, mock, verify, when } from 'ts-mockito';
import { of, throwError } from 'rxjs';
import { anything, instance, mock, verify, when } from 'ts-mockito';

import { ApiService } from 'ish-core/services/api/api.service';
import { makeHttpError } from 'ish-core/utils/dev/api-service-utils';

import { NewsletterService } from './newsletter.service';

Expand All @@ -18,26 +19,61 @@ describe('Newsletter Service', () => {
userEmail = 'user@test.com';
});

it("should subscribe user to newsletter when 'subscribeToNewsletter' is called", done => {
when(apiServiceMock.post(anyString(), anything())).thenReturn(of(true));
it("should subscribe user to newsletter when 'updateNewsletterSubscriptionStatus' is called with 'true'", done => {
when(apiServiceMock.post(anything(), anything())).thenReturn(of(true));

newsletterServiceMock.subscribeToNewsletter(userEmail).subscribe(subscriptionStatus => {
verify(apiServiceMock.post(`subscriptions`, anything())).once();
expect(subscriptionStatus).toBeTrue();
done();
});
const currentStatus = false;
const newStatus = true;

newsletterServiceMock
.updateNewsletterSubscriptionStatus(newStatus, currentStatus, 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));

const currentStatus = true;
const newStatus = false;

newsletterServiceMock
.updateNewsletterSubscriptionStatus(newStatus, currentStatus, 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));

const newStatus = true;
const currentStatus = true;

newsletterServiceMock
.updateNewsletterSubscriptionStatus(newStatus, currentStatus, 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(anyString())).thenReturn(of({ active: true }));
when(apiServiceMock.get(anything())).thenReturn(of({ active: true }));

newsletterServiceMock.getSubscription(userEmail).subscribe(subscriptionStatus => {
verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once();
expect(subscriptionStatus).toBeTrue();
done();
});

when(apiServiceMock.get(anyString())).thenReturn(of({ active: false }));
when(apiServiceMock.get(anything())).thenReturn(of({ active: false }));

newsletterServiceMock.getSubscription(userEmail).subscribe(subscriptionStatus => {
verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once();
Expand All @@ -46,11 +82,21 @@ describe('Newsletter Service', () => {
});
});

it("should unsubscribe user from newsletter when 'unsubscribeFromNewsletter' is called", done => {
when(apiServiceMock.delete(anyString())).thenReturn(of(false));
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 }))
);

newsletterServiceMock.getSubscription(userEmail).subscribe(subscriptionStatus => {
verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once();
expect(subscriptionStatus).toBeFalse();
done();
});

when(apiServiceMock.get(anything())).thenReturn(of({ active: false }));

newsletterServiceMock.unsubscribeFromNewsletter(userEmail).subscribe(subscriptionStatus => {
verify(apiServiceMock.delete(`subscriptions/${userEmail}`)).once();
newsletterServiceMock.getSubscription(userEmail).subscribe(subscriptionStatus => {
verify(apiServiceMock.get(`subscriptions/${userEmail}`)).once();
expect(subscriptionStatus).toBeFalse();
done();
});
Expand Down
44 changes: 40 additions & 4 deletions src/app/core/services/newsletter/newsletter.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
import { Injectable } from '@angular/core';
import { Observable, map } from 'rxjs';
import { Observable, catchError, map, of, throwError } from 'rxjs';

import { ApiService } from 'ish-core/services/api/api.service';

@Injectable({ providedIn: 'root' })
export class NewsletterService {
constructor(private apiService: ApiService) {}

/**
* 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<boolean> {
return this.apiService.get(`subscriptions/${userEmail}`).pipe(map((params: { active: boolean }) => params.active));
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);
})
);
}

subscribeToNewsletter(userEmail: string): Observable<boolean> {
// TODO: check if there is a better way to open a stream on the currentStatus instead of passing it through the effect
updateNewsletterSubscriptionStatus(
newStatus: boolean,
currentStatus: boolean,
userEmail: string
): Observable<boolean> {
// only make a REST-call when the status has changed
if (currentStatus === newStatus) {
return of(currentStatus);
}

if (newStatus) {
return this.subscribeToNewsletter(userEmail);
} else {
return this.unsubscribeFromNewsletter(userEmail);
}
}

/**
* always returns 'true'
*/
private subscribeToNewsletter(userEmail: string): Observable<boolean> {
const body = {
name: 'Newsletter',
type: 'Subscription',
Expand All @@ -22,7 +55,10 @@ export class NewsletterService {
return this.apiService.post(`subscriptions`, body).pipe(map(() => true));
}

unsubscribeFromNewsletter(userEmail: string): Observable<boolean> {
/**
* always returns 'false'
*/
private unsubscribeFromNewsletter(userEmail: string): Observable<boolean> {
return this.apiService.delete(`subscriptions/${userEmail}`).pipe(map(() => false));
}
}
71 changes: 39 additions & 32 deletions src/app/core/store/customer/user/user-newsletter.effects.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Store, select } from '@ngrx/store';
import { concatMap, filter, map, withLatestFrom } from 'rxjs/operators';
import { concatMap, filter, map, switchMap, withLatestFrom } from 'rxjs/operators';

import { NewsletterService } from 'ish-core/services/newsletter/newsletter.service';
import { mapErrorToAction, mapToPayloadProperty } from 'ish-core/utils/operators';
import { getServerConfigParameter } from 'ish-core/store/core/server-config';
import { mapErrorToAction, mapToPayload, whenTruthy } from 'ish-core/utils/operators';

import { userNewsletterActions, userNewsletterApiActions } from './user.actions';
import { getLoggedInUser } from './user.selectors';
import { getLoggedInUser, getNewsletterSubscriptionStatus } from './user.selectors';

@Injectable()
export class UserNewsletterEffects {
constructor(private actions$: Actions, private store: Store, private newsletterService: NewsletterService) {}

/**
* The newsletter-subscription-status is only loaded when the feature-toggle "newsletterSubscriptionEnabled"
* is enabled.
*/
loadUserNewsletterSubscription$ = createEffect(() =>
this.actions$.pipe(
ofType(userNewsletterActions.loadUserNewsletterSubscription),
withLatestFrom(this.store.pipe(select(getLoggedInUser))),
concatMap(([, user]) =>
this.newsletterService.getSubscription(user.email).pipe(
map(subscriptionStatus =>
userNewsletterApiActions.loadUserNewsletterSubscriptionSuccess({ subscribed: subscriptionStatus })
),
mapErrorToAction(userNewsletterApiActions.loadUserNewsletterSubscriptionFail)
withLatestFrom(
this.store.pipe(select(getServerConfigParameter<boolean>('marketing.newsletterSubscriptionEnabled')))
),
filter(([, newsletterSubscriptionEnabled]) => newsletterSubscriptionEnabled),
switchMap(() =>
this.store.pipe(select(getLoggedInUser)).pipe(
whenTruthy(),
concatMap(user =>
this.newsletterService.getSubscription(user.email).pipe(
map(subscriptionStatus =>
userNewsletterApiActions.loadUserNewsletterSubscriptionSuccess({ subscribed: subscriptionStatus })
),
mapErrorToAction(userNewsletterApiActions.loadUserNewsletterSubscriptionFail)
)
)
)
)
)
Expand All @@ -35,31 +48,25 @@ export class UserNewsletterEffects {
*/
subscribeUserToNewsletter$ = createEffect(() =>
this.actions$.pipe(
ofType(userNewsletterActions.subscribeUserToNewsletter),
mapToPayloadProperty('userEmail'),
withLatestFrom(this.store.pipe(select(getLoggedInUser))),
filter(([userEmail, user]) => !!userEmail || !!user?.email),
concatMap(([userEmail, user]) =>
ofType(userNewsletterActions.updateUserNewsletterStatus),
mapToPayload(),
withLatestFrom(
this.store.pipe(select(getLoggedInUser)),
this.store.pipe(select(getNewsletterSubscriptionStatus))
),
filter(([payload, user]) => !!payload.userEmail || !!user?.email),
concatMap(([payload, user, currentNewsletterSubscriptionStatus]) =>
this.newsletterService
.subscribeToNewsletter(userEmail || user.email)
.pipe(
map(userNewsletterApiActions.subscribeUserToNewsletterSuccess),
mapErrorToAction(userNewsletterApiActions.subscribeUserToNewsletterFail)
.updateNewsletterSubscriptionStatus(
payload.subscriptionStatus,
currentNewsletterSubscriptionStatus,
payload.userEmail || user.email
)
)
)
);

unsubscribeUserFromNewsletter$ = createEffect(() =>
this.actions$.pipe(
ofType(userNewsletterActions.unsubscribeUserFromNewsletter),
withLatestFrom(this.store.pipe(select(getLoggedInUser))),
concatMap(([, user]) =>
this.newsletterService
.unsubscribeFromNewsletter(user.email)
.pipe(
map(userNewsletterApiActions.unsubscribeUserFromNewsletterSuccess),
mapErrorToAction(userNewsletterApiActions.unsubscribeUserFromNewsletterFail)
map(subscriptionStatus =>
userNewsletterApiActions.updateUserNewsletterStatusSuccess({ subscriptionStatus })
),
mapErrorToAction(userNewsletterApiActions.updateUserNewsletterStatusFail)
)
)
)
Expand Down
9 changes: 3 additions & 6 deletions src/app/core/store/customer/user/user.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ export const userNewsletterActions = createActionGroup({
source: 'User Newsletter',
events: {
'Load User Newsletter Subscription': emptyProps(),
'Subscribe User To Newsletter': payload<{ userEmail?: string }>(),
'Unsubscribe User From Newsletter': emptyProps(),
'Update User Newsletter Status': payload<{ subscriptionStatus: boolean; userEmail?: string }>(),
},
});

Expand All @@ -161,9 +160,7 @@ export const userNewsletterApiActions = createActionGroup({
events: {
'Load User Newsletter Subscription Success': payload<{ subscribed: boolean }>(),
'Load User Newsletter Subscription Fail': httpError<{}>(),
'Subscribe User To Newsletter Success': emptyProps(),
'Subscribe User To Newsletter Fail': httpError<{}>(),
'Unsubscribe User From Newsletter Success': emptyProps(),
'Unsubscribe User From Newsletter Fail': httpError<{}>(),
'Update User Newsletter Status Success': payload<{ subscriptionStatus: boolean }>(),
'Update User Newsletter Status Fail': httpError<{}>(),
},
});
7 changes: 6 additions & 1 deletion src/app/core/store/customer/user/user.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,12 @@ export class UserEffects {
concatMap(([createUserResponse, customerTypeForLoginApproval]) => [
createUserSuccess({ email: createUserResponse.user.email }),
...(data.subscribedToNewsletter
? [userNewsletterActions.subscribeUserToNewsletter({ userEmail: createUserResponse.user.email })]
? [
userNewsletterActions.updateUserNewsletterStatus({
subscriptionStatus: true,
userEmail: createUserResponse.user.email,
}),
]
: []),
customerTypeForLoginApproval?.includes(createUserResponse.customer.isBusinessCustomer ? 'SMB' : 'PRIVATE')
? createUserApprovalRequired({ email: createUserResponse.user.email })
Expand Down
Loading

0 comments on commit 13d2b63

Please sign in to comment.