Skip to content

Commit

Permalink
feat: save punchout information in session storage instead of in cook…
Browse files Browse the repository at this point in the history
…ies (#1731)

* to avoid problems with punchout clients that are not able to set cookies, we now save the punchout information in session storage
* improved error handling (error in log and error message in UI) for missing punchout date supposed to be saved in session storage
  • Loading branch information
shauke committed Jan 14, 2025
1 parent 3eb3e14 commit 02051ca
Show file tree
Hide file tree
Showing 7 changed files with 44 additions and 70 deletions.
18 changes: 5 additions & 13 deletions docs/guides/cookie-consent.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ cookieConsentOptions: {
'apiToken',
'cookieConsent',
'preferredLocale',
'punchout_SID',
'punchout_BasketID',
'punchout_ReturnURL',
'punchout_HookURL',
],
},
```
Expand Down Expand Up @@ -108,15 +104,11 @@ This route can be linked to from anywhere within the application.

## PWA Required Cookies

| Name | Expiration | Provider | Description | Category |
| ------------------ | ---------- | ------------- | ----------------------------------------------------------------- | ----------- |
| apiToken | 1 hour | Intershop PWA | The API token used by the Intershop Commerce Management REST API. | First Party |
| cookieConsent | 1 year | Intershop PWA | Saves the user's cookie consent settings. | First Party |
| preferredLocale | 1 year | Intershop PWA | Saves the user's language selection. | First Party |
| punchout_SID | Session | Intershop PWA | Saves punchout session related data - Session ID. | First Party |
| punchout_BasketID | Session | Intershop PWA | Saves punchout session related data - Basket ID. | First Party |
| punchout_ReturnURL | Session | Intershop PWA | Saves punchout session related data - Return URL. | First Party |
| punchout_HookURL | Session | Intershop PWA | Saves punchout session related data - Hook URL. | First Party |
| Name | Expiration | Provider | Description | Category |
| --------------- | ---------- | ------------- | ----------------------------------------------------------------- | ----------- |
| apiToken | 1 hour | Intershop PWA | The API token used by the Intershop Commerce Management REST API. | First Party |
| cookieConsent | 1 year | Intershop PWA | Saves the user's cookie consent settings. | First Party |
| preferredLocale | 1 year | Intershop PWA | Saves the user's language selection. | First Party |

## Disabling the Integrated Cookie Consent Handling

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
import { TokenService } from 'ish-core/services/token/token.service';
import { selectQueryParam } from 'ish-core/store/core/router';
import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service';
import { CookiesService } from 'ish-core/utils/cookies/cookies.service';
import { makeHttpError } from 'ish-core/utils/dev/api-service-utils';
import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data';

Expand All @@ -32,7 +31,6 @@ describe('Punchout Identity Provider', () => {
const appFacade = mock(AppFacade);
const accountFacade = mock(AccountFacade);
const checkoutFacade = mock(CheckoutFacade);
const cookiesService = mock(CookiesService);

let punchoutIdentityProvider: PunchoutIdentityProvider;
let store$: MockStore;
Expand All @@ -45,7 +43,6 @@ describe('Punchout Identity Provider', () => {
{ provide: ApiTokenService, useFactory: () => instance(apiTokenService) },
{ provide: AppFacade, useFactory: () => instance(appFacade) },
{ provide: CheckoutFacade, useFactory: () => instance(checkoutFacade) },
{ provide: CookiesService, useFactory: () => instance(cookiesService) },
{ provide: PunchoutService, useFactory: () => instance(punchoutService) },
{ provide: TokenService, useFactory: () => instance(mock(TokenService)) },
provideMockStore(),
Expand All @@ -67,9 +64,8 @@ describe('Punchout Identity Provider', () => {
resetCalls(appFacade);
resetCalls(accountFacade);
resetCalls(checkoutFacade);
resetCalls(cookiesService);

window.sessionStorage.clear();
sessionStorage.clear();
});

describe('init', () => {
Expand All @@ -82,7 +78,7 @@ describe('Punchout Identity Provider', () => {
it('should add basket-id to session storage, when basket is available', () => {
when(checkoutFacade.basket$).thenReturn(of(BasketMockData.getBasket()));
punchoutIdentityProvider.init();
expect(window.sessionStorage.getItem('basket-id')).toEqual(BasketMockData.getBasket().id);
expect(sessionStorage.getItem('basket-id')).toEqual(BasketMockData.getBasket().id);
});
});

Expand All @@ -95,12 +91,12 @@ describe('Punchout Identity Provider', () => {
});

it('should remove api token and basket-id on logout', done => {
expect(window.sessionStorage.getItem('basket-id')).toEqual(BasketMockData.getBasket().id);
expect(sessionStorage.getItem('basket-id')).toEqual(BasketMockData.getBasket().id);

const logoutTrigger$ = punchoutIdentityProvider.triggerLogout() as Observable<UrlTree>;

logoutTrigger$.subscribe(() => {
expect(window.sessionStorage.getItem('basket-id')).toBeNull();
expect(sessionStorage.getItem('basket-id')).toBeNull();
verify(accountFacade.logoutUser()).once();
done();
});
Expand Down Expand Up @@ -187,14 +183,14 @@ describe('Punchout Identity Provider', () => {
);
});

it('should set cookies, load basket basket and return to homepage', fakeAsync(() => {
it('should set session storage, load basket basket and return to homepage', fakeAsync(() => {
const login$ = punchoutIdentityProvider.triggerLogin(getSnapshot(queryParams)) as Observable<
boolean | UrlTree
>;
login$.subscribe(() => {
verify(cookiesService.put('punchout_SID', 'sid')).once();
verify(cookiesService.put('punchout_ReturnURL', 'home')).once();
verify(cookiesService.put('punchout_BasketID', 'basket-id')).once();
expect(sessionStorage.getItem('punchout_SID')).toEqual('sid');
expect(sessionStorage.getItem('punchout_ReturnURL')).toEqual('home');
expect(sessionStorage.getItem('punchout_BasketID')).toEqual('basket-id');
});

tick(500);
Expand All @@ -208,20 +204,20 @@ describe('Punchout Identity Provider', () => {
queryParams = { HOOK_URL: 'url', USERNAME: 'username', PASSWORD: 'password' };
});

it('should set cookie and create basket on login', done => {
it('should set session storage and create basket on login', done => {
const login$ = punchoutIdentityProvider.triggerLogin(getSnapshot(queryParams)) as Observable<
boolean | UrlTree
>;
login$.subscribe(() => {
verify(cookiesService.put('punchout_HookURL', 'url')).once();
expect(sessionStorage.getItem('punchout_HookURL')).toEqual('url');
verify(checkoutFacade.createBasket()).once();
expect(routerSpy).toHaveBeenCalledWith('/home');
done();
});
});

it('should reload basket when basket is saved in session storage', done => {
window.sessionStorage.setItem('basket-id', 'basket-id');
sessionStorage.setItem('basket-id', 'basket-id');
const login$ = punchoutIdentityProvider.triggerLogin(getSnapshot(queryParams)) as Observable<
boolean | UrlTree
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { IdentityProvider, TriggerReturnType } from 'ish-core/identity-provider/
import { TokenService } from 'ish-core/services/token/token.service';
import { selectQueryParam } from 'ish-core/store/core/router';
import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service';
import { CookiesService } from 'ish-core/utils/cookies/cookies.service';
import { whenTruthy } from 'ish-core/utils/operators';

import { PunchoutService } from '../services/punchout/punchout.service';
Expand All @@ -26,7 +25,6 @@ export class PunchoutIdentityProvider implements IdentityProvider {
private appFacade: AppFacade,
private accountFacade: AccountFacade,
private punchoutService: PunchoutService,
private cookiesService: CookiesService,
private checkoutFacade: CheckoutFacade,
private tokenService: TokenService
) {}
Expand All @@ -50,7 +48,7 @@ export class PunchoutIdentityProvider implements IdentityProvider {
this.apiTokenService.restore$(['user', 'order']).subscribe(noop);

this.checkoutFacade.basket$.pipe(whenTruthy(), first()).subscribe(basketView => {
window.sessionStorage.setItem('basket-id', basketView.id);
sessionStorage.setItem('basket-id', basketView.id);
});
}

Expand Down Expand Up @@ -123,7 +121,7 @@ export class PunchoutIdentityProvider implements IdentityProvider {
}

triggerLogout(): TriggerReturnType {
window.sessionStorage.removeItem('basket-id');
sessionStorage.removeItem('basket-id');
this.accountFacade.logoutUser(); // user will be logged out and related refresh token is revoked on server
return this.accountFacade.isLoggedIn$.pipe(
// wait until the user is logged out before you go to homepage to prevent unnecessary REST calls
Expand All @@ -147,11 +145,11 @@ export class PunchoutIdentityProvider implements IdentityProvider {
private handleCxmlPunchoutLogin(route: ActivatedRouteSnapshot): Observable<UrlTree> {
// fetch sid session information (basketId, returnURL, operation, ...)
return this.punchoutService.getCxmlPunchoutSession(route.queryParamMap.get('sid')).pipe(
// persist cXML session information (sid, returnURL, basketId) in cookies for later basket transfer
// persist cXML session information (sid, returnURL, basketId) in session storage for later basket transfer
tap(data => {
this.cookiesService.put('punchout_SID', route.queryParamMap.get('sid'));
this.cookiesService.put('punchout_ReturnURL', data.returnURL);
this.cookiesService.put('punchout_BasketID', data.basketId);
sessionStorage.setItem('punchout_SID', route.queryParamMap.get('sid'));
sessionStorage.setItem('punchout_ReturnURL', data.returnURL);
sessionStorage.setItem('punchout_BasketID', data.basketId);
}),
// use the basketId basket for the current PWA session (instead of default current basket)
// TODO: if load basket error (currently no error page) -> logout and do not use default 'current' basket
Expand All @@ -163,10 +161,10 @@ export class PunchoutIdentityProvider implements IdentityProvider {
}

private handleOciPunchoutLogin(route: ActivatedRouteSnapshot) {
// save HOOK_URL to cookie for later basket transfer
this.cookiesService.put('punchout_HookURL', route.queryParamMap.get('HOOK_URL'));
// save HOOK_URL to session storage for later basket transfer
sessionStorage.setItem('punchout_HookURL', route.queryParamMap.get('HOOK_URL'));

const basketId = window.sessionStorage.getItem('basket-id');
const basketId = sessionStorage.getItem('basket-id');
if (!basketId) {
// create a new basket for every punchout session to avoid basket conflicts for concurrent punchout sessions
this.checkoutFacade.createBasket();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,18 @@ import { anything, capture, instance, mock, verify, when } from 'ts-mockito';
import { Customer } from 'ish-core/models/customer/customer.model';
import { ApiService } from 'ish-core/services/api/api.service';
import { getLoggedInCustomer } from 'ish-core/store/customer/user';
import { CookiesService } from 'ish-core/utils/cookies/cookies.service';

import { PunchoutUser } from '../../models/punchout-user/punchout-user.model';

import { PunchoutService } from './punchout.service';

describe('Punchout Service', () => {
let apiServiceMock: ApiService;
let cookiesServiceMock: CookiesService;
let punchoutService: PunchoutService;
const punchoutUser = { login: 'ociuser', punchoutType: 'oci' } as PunchoutUser;

beforeEach(() => {
apiServiceMock = mock(ApiService);
cookiesServiceMock = mock(CookiesService);

when(apiServiceMock.options(anything(), anything())).thenReturn(of({}));
when(apiServiceMock.get(anything(), anything())).thenReturn(of({}));
Expand All @@ -35,7 +32,6 @@ describe('Punchout Service', () => {
TestBed.configureTestingModule({
providers: [
{ provide: ApiService, useFactory: () => instance(apiServiceMock) },
{ provide: CookiesService, useFactory: () => instance(cookiesServiceMock) },
provideMockStore({
selectors: [
{ selector: getLoggedInCustomer, value: { customerNo: '4711', isBusinessCustomer: true } as Customer },
Expand Down
34 changes: 17 additions & 17 deletions src/app/extensions/punchout/services/punchout/punchout.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { Store, select } from '@ngrx/store';
import { EMPTY, Observable, iif, throwError } from 'rxjs';
import { concatMap, filter, map, switchMap, take } from 'rxjs/operators';

import { MessageFacade } from 'ish-core/facades/message.facade';
import { Attribute } from 'ish-core/models/attribute/attribute.model';
import { Link } from 'ish-core/models/link/link.model';
import { ApiService, unpackEnvelope } from 'ish-core/services/api/api.service';
import { getUserPermissions } from 'ish-core/store/customer/authorization';
import { getCurrentBasketId } from 'ish-core/store/customer/basket';
import { getLoggedInCustomer } from 'ish-core/store/customer/user';
import { CookiesService } from 'ish-core/utils/cookies/cookies.service';
import { DomService } from 'ish-core/utils/dom/dom.service';
import { whenTruthy } from 'ish-core/utils/operators';

Expand All @@ -28,10 +28,10 @@ import { PunchoutType, PunchoutUser } from '../../models/punchout-user/punchout-
@Injectable({ providedIn: 'root' })
export class PunchoutService {
constructor(
private apiService: ApiService,
private cookiesService: CookiesService,
private store: Store,
private apiService: ApiService,
private domService: DomService,
private messageFacade: MessageFacade,
@Inject(DOCUMENT) private document: Document
) {}

Expand Down Expand Up @@ -208,7 +208,7 @@ export class PunchoutService {
*/
private submitPunchoutForm(form: HTMLFormElement, submit = true) {
if (!form) {
return throwError(() => new Error('submitPunchoutForm() of the punchout service called without a form'));
throw new Error('submitPunchoutForm() of the punchout service called without a form');
}

// replace the document content with the form and submit the form
Expand Down Expand Up @@ -242,15 +242,17 @@ export class PunchoutService {
* transferCxmlPunchoutBasket
*/
private transferCxmlPunchoutBasket() {
const punchoutSID = this.cookiesService.get('punchout_SID');
const punchoutSID = sessionStorage.getItem('punchout_SID');
if (!punchoutSID) {
return throwError(() => new Error('no punchout_SID available in cookies for cXML punchout basket transfer'));
const errorMessage = 'no punchout_SID information available for cXML punchout basket transfer';
this.messageFacade.error({ message: errorMessage });
return throwError(() => new Error(errorMessage));
}
const returnURL = this.cookiesService.get('punchout_ReturnURL');
const returnURL = sessionStorage.getItem('punchout_ReturnURL');
if (!returnURL) {
return throwError(
() => new Error('no punchout_ReturnURL available in cookies for cXML punchout basket transfer')
);
const errorMessage = 'no punchout_ReturnURL information available for cXML punchout basket transfer';
this.messageFacade.error({ message: errorMessage });
return throwError(() => new Error(errorMessage));
}

return this.currentCustomer$.pipe(
Expand All @@ -270,8 +272,6 @@ export class PunchoutService {
.pipe(map(data => this.submitPunchoutForm(this.createCxmlPunchoutForm(data, returnURL))))
)
);

// TODO: cleanup punchout cookies?
}

/**
Expand Down Expand Up @@ -453,13 +453,13 @@ export class PunchoutService {
*/
submitOciPunchoutData(data: Attribute<string>[], submit = true) {
if (!data?.length) {
return throwError(() => new Error('submitOciPunchoutData() of the punchout service called without data'));
throw new Error('submitOciPunchoutData() of the punchout service called without data');
}
const hookUrl = this.cookiesService.get('punchout_HookURL');
const hookUrl = sessionStorage.getItem('punchout_HookURL');
if (!hookUrl) {
return throwError(
() => new Error('no punchout_HookURL available in cookies for OCI Punchout submitPunchoutData()')
);
const errorMessage = 'no punchout_HookURL information available for OCI Punchout submitPunchoutData()';
this.messageFacade.error({ message: errorMessage });
throw new Error(errorMessage);
}
this.submitPunchoutForm(this.createOciForm(data, hookUrl), submit);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class PunchoutFunctionsEffects {
() =>
this.actions$.pipe(
ofType(transferPunchoutBasketSuccess),
map(() => window.sessionStorage.removeItem('basket-id'))
map(() => sessionStorage.removeItem('basket-id'))
),
{ dispatch: false }
);
Expand Down
10 changes: 1 addition & 9 deletions src/environments/environment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,7 @@ export const ENVIRONMENT_DEFAULTS: Omit<Environment, 'icmChannel'> = {
description: 'cookie.consent.option.tracking.description',
},
},
allowedCookies: [
'apiToken',
'cookieConsent',
'preferredLocale',
'punchout_SID',
'punchout_BasketID',
'punchout_ReturnURL',
'punchout_HookURL',
],
allowedCookies: ['apiToken', 'cookieConsent', 'preferredLocale'],
},
cookieConsentVersion: 1,

Expand Down

0 comments on commit 02051ca

Please sign in to comment.