-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: newsletter subscription (#1523)
Co-authored-by: Lucas Hengelhaupt <[email protected]> Co-authored-by: Silke <[email protected]> Co-authored-by: MGlatter <[email protected]> Co-authored-by: Stefan Hauke <[email protected]>
- Loading branch information
1 parent
1903d09
commit 68c0bc5
Showing
40 changed files
with
880 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<!-- | ||
kb_guide | ||
kb_pwa | ||
kb_everyone | ||
kb_sync_latest_only | ||
--> | ||
|
||
# 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
src/app/core/services/newsletter/newsletter.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = '[email protected]'; | ||
|
||
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<boolean> { | ||
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<boolean> { | ||
// 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<boolean> { | ||
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<boolean> { | ||
return this.apiService.delete(`subscriptions/${userEmail}`).pipe(map(() => false)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.