diff --git a/src/app/core/directives/not-role-toggle.directive.spec.ts b/src/app/core/directives/not-role-toggle.directive.spec.ts
new file mode 100644
index 0000000000..641c824c8b
--- /dev/null
+++ b/src/app/core/directives/not-role-toggle.directive.spec.ts
@@ -0,0 +1,76 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RoleToggleModule } from 'ish-core/role-toggle.module';
+
+@Component({
+ template: `
+
unrelated
+ content1
+ content2
+ content3
+ `,
+ // Default change detection for dynamic role test
+ changeDetection: ChangeDetectionStrategy.Default,
+})
+class TestComponent {
+ dynamicRole: string;
+}
+
+describe('Not Role Toggle Directive', () => {
+ let fixture: ComponentFixture;
+ let element: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [TestComponent],
+ imports: [RoleToggleModule.forTesting('ROLE1')],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ element = fixture.nativeElement;
+ fixture.detectChanges();
+ });
+
+ it('should always render unrelated content', () => {
+ expect(element.textContent).toContain('unrelated');
+ });
+
+ it('should not render content if role is given', () => {
+ expect(element.textContent).not.toContain('content1');
+ });
+
+ it('should render content if the role is not given', () => {
+ expect(element.textContent).toContain('content2');
+ });
+
+ it('should react on changing roles in store', () => {
+ RoleToggleModule.switchTestingRoles('ROLE2');
+ fixture.detectChanges();
+
+ expect(element.textContent).toContain('content1');
+ expect(element.textContent).not.toContain('content2');
+
+ RoleToggleModule.switchTestingRoles('ROLE1');
+ fixture.detectChanges();
+
+ expect(element.textContent).not.toContain('content1');
+ expect(element.textContent).toContain('content2');
+ });
+
+ it('should react on changing roles from input', () => {
+ expect(element.textContent).toContain('content3');
+
+ fixture.componentInstance.dynamicRole = 'ROLE1';
+ fixture.detectChanges();
+
+ expect(element.textContent).not.toContain('content3');
+
+ fixture.componentInstance.dynamicRole = 'ROLE2';
+ fixture.detectChanges();
+
+ expect(element.textContent).toContain('content3');
+ });
+});
diff --git a/src/app/core/directives/not-role-toggle.directive.ts b/src/app/core/directives/not-role-toggle.directive.ts
new file mode 100644
index 0000000000..cf4538f4ef
--- /dev/null
+++ b/src/app/core/directives/not-role-toggle.directive.ts
@@ -0,0 +1,55 @@
+import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
+import { ReplaySubject, Subject, Subscription } from 'rxjs';
+import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
+
+import { RoleToggleService } from 'ish-core/utils/role-toggle/role-toggle.service';
+
+/**
+ * Structural directive.
+ * Used on an element, this element will only be rendered if the logged in user has NOT the specified role.
+ *
+ * @example
+ *
+ * Only visible when the current user is not an OCI punchout user.
+ *
+ */
+@Directive({
+ selector: '[ishHasNotRole]',
+})
+export class NotRoleToggleDirective implements OnDestroy {
+ private subscription: Subscription;
+ private enabled$ = new ReplaySubject(1);
+
+ private destroy$ = new Subject();
+
+ constructor(
+ private templateRef: TemplateRef,
+ private viewContainer: ViewContainerRef,
+ private roleToggleService: RoleToggleService
+ ) {
+ this.enabled$.pipe(distinctUntilChanged(), takeUntil(this.destroy$)).subscribe(enabled => {
+ if (enabled) {
+ this.viewContainer.createEmbeddedView(this.templateRef);
+ } else {
+ this.viewContainer.clear();
+ }
+ });
+ }
+
+ @Input() set ishHasNotRole(roleId: string) {
+ // end previous subscription and newly subscribe
+ if (this.subscription) {
+ // tslint:disable-next-line: ban
+ this.subscription.unsubscribe();
+ }
+ this.subscription = this.roleToggleService
+ .hasRole(roleId)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({ next: val => this.enabled$.next(!val) });
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+}
diff --git a/src/app/core/role-toggle.module.ts b/src/app/core/role-toggle.module.ts
new file mode 100644
index 0000000000..7e6d03841a
--- /dev/null
+++ b/src/app/core/role-toggle.module.ts
@@ -0,0 +1,40 @@
+import { ModuleWithProviders, NgModule } from '@angular/core';
+import { ReplaySubject } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { NotRoleToggleDirective } from './directives/not-role-toggle.directive';
+import { whenTruthy } from './utils/operators';
+import { RoleToggleService, checkRole } from './utils/role-toggle/role-toggle.service';
+
+@NgModule({
+ declarations: [NotRoleToggleDirective],
+ exports: [NotRoleToggleDirective],
+})
+export class RoleToggleModule {
+ private static roleIds = new ReplaySubject(1);
+
+ static forTesting(...roleIds: string[]): ModuleWithProviders {
+ RoleToggleModule.switchTestingRoles(...roleIds);
+ return {
+ ngModule: RoleToggleModule,
+ providers: [
+ {
+ provide: RoleToggleService,
+ useValue: {
+ hasRole: (roleId: string) =>
+ RoleToggleModule.roleIds.pipe(
+ whenTruthy(),
+ map(roles => checkRole(roles, roleId))
+ ),
+ },
+ },
+ ],
+ };
+ }
+
+ static switchTestingRoles(...roleIds: string[]) {
+ RoleToggleModule.roleIds.next(roleIds);
+ }
+}
+
+export { RoleToggleService } from './utils/role-toggle/role-toggle.service';
diff --git a/src/app/core/utils/role-toggle/role-toggle.service.spec.ts b/src/app/core/utils/role-toggle/role-toggle.service.spec.ts
new file mode 100644
index 0000000000..cae4368bd3
--- /dev/null
+++ b/src/app/core/utils/role-toggle/role-toggle.service.spec.ts
@@ -0,0 +1,39 @@
+import { TestBed } from '@angular/core/testing';
+import { Store } from '@ngrx/store';
+import { cold } from 'jest-marbles';
+
+import { CoreStoreModule } from 'ish-core/store/core/core-store.module';
+import { loadRolesAndPermissionsSuccess } from 'ish-core/store/customer/authorization';
+import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module';
+
+import { RoleToggleService } from './role-toggle.service';
+
+describe('Role Toggle Service', () => {
+ let store$: Store;
+ let roleToggleService: RoleToggleService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [CoreStoreModule.forTesting(), CustomerStoreModule.forTesting('authorization')],
+ });
+
+ store$ = TestBed.inject(Store);
+ store$.dispatch(
+ loadRolesAndPermissionsSuccess({
+ authorization: { roles: [{ roleId: 'ROLE1', displayName: 'Role 1' }], permissionIDs: [] },
+ })
+ );
+
+ roleToggleService = TestBed.inject(RoleToggleService);
+ });
+
+ describe('hasRole', () => {
+ it('should return true if user has the role', () => {
+ expect(roleToggleService.hasRole('ROLE1')).toBeObservable(cold('a', { a: true }));
+ });
+
+ it("should return false if user doesn't have the role", () => {
+ expect(roleToggleService.hasRole('ROLE2')).toBeObservable(cold('a', { a: false }));
+ });
+ });
+});
diff --git a/src/app/core/utils/role-toggle/role-toggle.service.ts b/src/app/core/utils/role-toggle/role-toggle.service.ts
new file mode 100644
index 0000000000..ba2fa55e76
--- /dev/null
+++ b/src/app/core/utils/role-toggle/role-toggle.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import { Store, select } from '@ngrx/store';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { getUserRoles } from 'ish-core/store/customer/authorization';
+import { whenTruthy } from 'ish-core/utils/operators';
+
+export function checkRole(roleIds: string[], roleId: string): boolean {
+ return roleIds.includes(roleId);
+}
+
+@Injectable({ providedIn: 'root' })
+export class RoleToggleService {
+ private roleIds$: Observable;
+
+ constructor(store: Store) {
+ this.roleIds$ = store.pipe(select(getUserRoles)).pipe(map(roles => roles.map(role => role.roleId)));
+ }
+
+ hasRole(roleId: string): Observable {
+ return this.roleIds$.pipe(
+ // wait for permissions to be loaded
+ whenTruthy(),
+ map(roleIds => checkRole(roleIds, roleId))
+ );
+ }
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 2e44bb85bc..8b9a6425a8 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -19,6 +19,7 @@ import { AuthorizationToggleModule } from 'ish-core/authorization-toggle.module'
import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { IconModule } from 'ish-core/icon.module';
import { PipesModule } from 'ish-core/pipes.module';
+import { RoleToggleModule } from 'ish-core/role-toggle.module';
import { ShellModule } from 'ish-shell/shell.module';
import { AddressFormsSharedModule } from './address-forms/address-forms.module';
@@ -129,6 +130,7 @@ const importExportModules = [
NgbPopoverModule,
PipesModule,
ReactiveFormsModule,
+ RoleToggleModule,
RouterModule,
ShellModule,
SwiperModule,
diff --git a/src/app/shell/shell.module.ts b/src/app/shell/shell.module.ts
index 1147bf0900..2391d7b5df 100644
--- a/src/app/shell/shell.module.ts
+++ b/src/app/shell/shell.module.ts
@@ -10,6 +10,7 @@ import { DirectivesModule } from 'ish-core/directives.module';
import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { IconModule } from 'ish-core/icon.module';
import { PipesModule } from 'ish-core/pipes.module';
+import { RoleToggleModule } from 'ish-core/role-toggle.module';
import { CaptchaExportsModule } from '../extensions/captcha/exports/captcha-exports.module';
import { OrderTemplatesExportsModule } from '../extensions/order-templates/exports/order-templates-exports.module';
@@ -40,6 +41,7 @@ const importExportModules = [
OrderTemplatesExportsModule,
QuickorderExportsModule,
QuotingExportsModule,
+ RoleToggleModule,
TactonExportsModule,
WishlistsExportsModule,
];