-
Notifications
You must be signed in to change notification settings - Fork 21
Feat(ENG-8778): Implement Cookie consent message #353
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
<router-outlet /> | ||
<osf-toast></osf-toast> | ||
<osf-cookie-consent></osf-cookie-consent> | ||
<osf-full-screen-loader></osf-full-screen-loader> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<p-toast key="cookie" position="bottom-center" [styleClass]="'cookie-toast'"> | ||
<ng-template let-message pTemplate="message"> | ||
<div class="flex flex-column gap-2"> | ||
<span>{{ message.detail }}</span> | ||
<div class="flex justify-content-end"> | ||
<p-button | ||
type="button" | ||
label="{{ 'toast.cookie-consent.accept' | translate }}" | ||
(click)="acceptCookies()" | ||
></p-button> | ||
</div> | ||
</div> | ||
</ng-template> | ||
</p-toast> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
:host ::ng-deep .cookie-toast { | ||
width: 900px; | ||
max-width: min(92vw, 960px); | ||
left: 50% !important; | ||
transform: translateX(-50%) !important; | ||
} | ||
|
||
:host ::ng-deep .cookie-toast .p-toast-message { | ||
width: 100%; | ||
} | ||
|
||
:host ::ng-deep .cookie-toast .p-toast-message .p-toast-message-content { | ||
color: #fcf8e3; | ||
width: 100%; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { TranslateService } from '@ngx-translate/core'; | ||
|
||
import { MessageService } from 'primeng/api'; | ||
|
||
import { of } from 'rxjs'; | ||
|
||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
|
||
import { CookieConsentService } from '../../services/cookie-consent/cookie-consent.service'; | ||
|
||
import { CookieConsentComponent } from './cookie-consent.component'; | ||
|
||
describe('CookieConsentComponent', () => { | ||
let component: CookieConsentComponent; | ||
let fixture: ComponentFixture<CookieConsentComponent>; | ||
let mockToastService: jest.Mocked<MessageService>; | ||
let mockConsentService: jest.Mocked<CookieConsentService>; | ||
let mockTranslateService: jest.Mocked<TranslateService>; | ||
|
||
beforeEach(async () => { | ||
mockToastService = { | ||
add: jest.fn(), | ||
clear: jest.fn(), | ||
} as unknown as jest.Mocked<MessageService>; | ||
|
||
mockConsentService = { | ||
hasConsent: jest.fn(), | ||
grantConsent: jest.fn(), | ||
} as unknown as jest.Mocked<CookieConsentService>; | ||
|
||
mockTranslateService = { | ||
get: jest.fn(), | ||
} as unknown as jest.Mocked<TranslateService>; | ||
|
||
await TestBed.configureTestingModule({ | ||
imports: [CookieConsentComponent], | ||
providers: [ | ||
{ provide: MessageService, useValue: mockToastService }, | ||
{ provide: CookieConsentService, useValue: mockConsentService }, | ||
{ provide: TranslateService, useValue: mockTranslateService }, | ||
], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(CookieConsentComponent); | ||
component = fixture.componentInstance; | ||
}); | ||
describe('ngAfterViewInit', () => { | ||
it('should show toast if no consent', () => { | ||
mockConsentService.hasConsent.mockReturnValue(false); | ||
mockTranslateService.get.mockReturnValue(of('Please accept cookies')); | ||
|
||
component.ngAfterViewInit(); | ||
|
||
// wait for queueMicrotask to execute | ||
return Promise.resolve().then(() => { | ||
expect(mockTranslateService.get).toHaveBeenCalledWith('toast.cookie-consent.message'); | ||
expect(mockToastService.add).toHaveBeenCalledWith({ | ||
detail: 'Please accept cookies', | ||
key: 'cookie', | ||
sticky: true, | ||
severity: 'warn', | ||
closable: false, | ||
}); | ||
}); | ||
}); | ||
|
||
it('should not show toast if consent already given', () => { | ||
mockConsentService.hasConsent.mockReturnValue(true); | ||
|
||
component.ngAfterViewInit(); | ||
|
||
expect(mockTranslateService.get).not.toHaveBeenCalled(); | ||
expect(mockToastService.add).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('acceptCookies', () => { | ||
it('should grant consent and clear toast', () => { | ||
component.acceptCookies(); | ||
expect(mockConsentService.grantConsent).toHaveBeenCalled(); | ||
expect(mockToastService.clear).toHaveBeenCalledWith('cookie'); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { TranslateModule, TranslateService } from '@ngx-translate/core'; | ||
|
||
import { MessageService, PrimeTemplate } from 'primeng/api'; | ||
import { Button } from 'primeng/button'; | ||
import { Toast } from 'primeng/toast'; | ||
|
||
import { AfterViewInit, Component, inject } from '@angular/core'; | ||
|
||
import { CookieConsentService } from '../../services/cookie-consent/cookie-consent.service'; | ||
|
||
@Component({ | ||
selector: 'osf-cookie-consent', | ||
standalone: true, | ||
templateUrl: './cookie-consent.component.html', | ||
styleUrls: ['./cookie-consent.component.scss'], | ||
imports: [Toast, Button, PrimeTemplate, TranslateModule], | ||
}) | ||
export class CookieConsentComponent implements AfterViewInit { | ||
private readonly toastService = inject(MessageService); | ||
private readonly consentService = inject(CookieConsentService); | ||
private readonly translateService = inject(TranslateService); | ||
|
||
ngAfterViewInit() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this in ngAfterViewInit()? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both this and microtask are needed because otherwise toast service will be invoked before view is initialized, and therefore won't be displayed. |
||
if (!this.consentService.hasConsent()) { | ||
this.translateService.get('toast.cookie-consent.message').subscribe((detail) => { | ||
queueMicrotask(() => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need a microtask? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both this and ngAfterViewInit are needed because otherwise toast service will be invoked before view is initialized, and therefore won't be displayed. |
||
this.toastService.add({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need a banner and toast? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I don't understand. This PR displays only only the toast. |
||
detail, | ||
key: 'cookie', | ||
sticky: true, | ||
severity: 'warn', | ||
closable: false, | ||
}) | ||
); | ||
}); | ||
} | ||
} | ||
|
||
acceptCookies() { | ||
this.consentService.grantConsent(); | ||
this.toastService.clear('cookie'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
<p-toast class="toast-container" position="top-right" [preventOpenDuplicates]="true"> | ||
<p-toast class="toast-container" position="top-right" [preventOpenDuplicates]="true" key="osf"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you need to add a "key"? And what does it do? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. key attribute differentiates between default toast and cookie consent toast, which must be different because cookie consent toast contains a button, and default one does not |
||
<ng-template let-message pTemplate="message"> | ||
<div class="font-medium w-full">{{ message.summary | translate: message.data?.translationParams }}</div> | ||
</ng-template> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
|
||
import { CookieConsentService } from './cookie-consent.service'; | ||
|
||
describe('CookieConsentService', () => { | ||
let service: CookieConsentService; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({}); | ||
service = TestBed.inject(CookieConsentService); | ||
|
||
const store: Record<string, string> = {}; | ||
jest.spyOn(localStorage, 'getItem').mockImplementation((key: string) => store[key] || null); | ||
jest.spyOn(localStorage, 'setItem').mockImplementation((key: string, value: string) => { | ||
store[key] = value; | ||
}); | ||
jest.spyOn(localStorage, 'removeItem').mockImplementation((key: string) => { | ||
delete store[key]; | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it('should return false if no consent is stored', () => { | ||
expect(service.hasConsent()).toBe(false); | ||
}); | ||
|
||
it('should return true after consent is granted', () => { | ||
service.grantConsent(); | ||
expect(service.hasConsent()).toBe(true); | ||
}); | ||
|
||
it('should remove consent when revoked', () => { | ||
service.grantConsent(); | ||
expect(service.hasConsent()).toBe(true); | ||
|
||
service.revokeConsent(); | ||
expect(service.hasConsent()).toBe(false); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Injectable } from '@angular/core'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class CookieConsentService { | ||
private consentKey = 'cookie-consent'; | ||
|
||
hasConsent(): boolean { | ||
return localStorage.getItem(this.consentKey) === 'true'; | ||
} | ||
|
||
grantConsent() { | ||
localStorage.setItem(this.consentKey, 'true'); | ||
} | ||
|
||
revokeConsent() { | ||
localStorage.removeItem(this.consentKey); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will display every time. I think it needs an @if
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't be displayed every time, toasts are diplayed only when
MessageService
is invoked