diff --git a/angular/src/components/modal.component.ts b/angular/src/components/modal.component.ts
deleted file mode 100644
index 9e4e85b88b..0000000000
--- a/angular/src/components/modal.component.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- Component,
- ComponentFactoryResolver,
- EventEmitter,
- OnDestroy,
- Output,
- Type,
- ViewChild,
- ViewContainerRef,
-} from '@angular/core';
-
-import { MessagingService } from 'jslib-common/abstractions/messaging.service';
-
-@Component({
- selector: 'app-modal',
- template: ``,
-})
-export class ModalComponent implements OnDestroy {
- @Output() onClose = new EventEmitter();
- @Output() onClosed = new EventEmitter();
- @Output() onShow = new EventEmitter();
- @Output() onShown = new EventEmitter();
- @ViewChild('container', { read: ViewContainerRef, static: true }) container: ViewContainerRef;
- parentContainer: ViewContainerRef = null;
- fade: boolean = true;
-
- constructor(protected componentFactoryResolver: ComponentFactoryResolver,
- protected messagingService: MessagingService) { }
-
- ngOnDestroy() {
- document.body.classList.remove('modal-open');
- document.body.removeChild(document.querySelector('.modal-backdrop'));
- }
-
- show(type: Type, parentContainer: ViewContainerRef, fade: boolean = true,
- setComponentParameters: (component: T) => void = null): T {
- this.onShow.emit();
- this.messagingService.send('modalShow');
- this.parentContainer = parentContainer;
- this.fade = fade;
-
- document.body.classList.add('modal-open');
- const backdrop = document.createElement('div');
- backdrop.className = 'modal-backdrop' + (this.fade ? ' fade' : '');
- document.body.appendChild(backdrop);
-
- const factory = this.componentFactoryResolver.resolveComponentFactory(type);
- const componentRef = this.container.createComponent(factory);
- if (setComponentParameters != null) {
- setComponentParameters(componentRef.instance);
- }
-
- document.querySelector('.modal-dialog').addEventListener('click', (e: Event) => {
- e.stopPropagation();
- });
-
- const modals = Array.from(document.querySelectorAll('.modal, .modal *[data-dismiss="modal"]'));
- for (const closeElement of modals) {
- closeElement.addEventListener('click', event => {
- this.close();
- });
- }
-
- this.onShown.emit();
- this.messagingService.send('modalShown');
- return componentRef.instance;
- }
-
- close() {
- this.onClose.emit();
- this.messagingService.send('modalClose');
- this.onClosed.emit();
- this.messagingService.send('modalClosed');
- if (this.parentContainer != null) {
- this.parentContainer.clear();
- }
- }
-}
diff --git a/angular/src/components/modal/dynamic-modal.component.ts b/angular/src/components/modal/dynamic-modal.component.ts
new file mode 100644
index 0000000000..d6deb99ded
--- /dev/null
+++ b/angular/src/components/modal/dynamic-modal.component.ts
@@ -0,0 +1,57 @@
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ ComponentFactoryResolver,
+ ComponentRef,
+ ElementRef,
+ OnDestroy,
+ Type,
+ ViewChild,
+ ViewContainerRef
+} from '@angular/core';
+
+import { ModalRef } from './modal.ref';
+
+@Component({
+ selector: 'app-modal',
+ template: '',
+})
+export class DynamicModalComponent implements AfterViewInit, OnDestroy {
+ componentRef: ComponentRef;
+
+ @ViewChild('modalContent', { read: ViewContainerRef, static: true }) modalContentRef: ViewContainerRef;
+
+ childComponentType: Type;
+ setComponentParameters: (component: any) => void;
+
+ constructor(private componentFactoryResolver: ComponentFactoryResolver, private cd: ChangeDetectorRef,
+ private el: ElementRef, public modalRef: ModalRef) {}
+
+ ngAfterViewInit() {
+ this.loadChildComponent(this.childComponentType);
+ if (this.setComponentParameters != null) {
+ this.setComponentParameters(this.componentRef.instance);
+ }
+ this.cd.detectChanges();
+
+ this.modalRef.created(this.el.nativeElement);
+ }
+
+ loadChildComponent(componentType: Type) {
+ const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType);
+
+ this.modalContentRef.clear();
+ this.componentRef = this.modalContentRef.createComponent(componentFactory);
+ }
+
+ ngOnDestroy() {
+ if (this.componentRef) {
+ this.componentRef.destroy();
+ }
+ }
+
+ close() {
+ this.modalRef.close();
+ }
+}
diff --git a/angular/src/components/modal/modal-injector.ts b/angular/src/components/modal/modal-injector.ts
new file mode 100644
index 0000000000..8477608e81
--- /dev/null
+++ b/angular/src/components/modal/modal-injector.ts
@@ -0,0 +1,15 @@
+import {
+ InjectFlags,
+ InjectionToken,
+ Injector,
+ Type
+} from '@angular/core';
+
+export class ModalInjector implements Injector {
+ constructor(private _parentInjector: Injector, private _additionalTokens: WeakMap) {}
+
+ get(token: Type | InjectionToken, notFoundValue?: T, flags?: InjectFlags): T;
+ get(token: any, notFoundValue?: any, flags?: any) {
+ return this._additionalTokens.get(token) ?? this._parentInjector.get(token, notFoundValue);
+ }
+}
diff --git a/angular/src/components/modal/modal.ref.ts b/angular/src/components/modal/modal.ref.ts
new file mode 100644
index 0000000000..116a6057c8
--- /dev/null
+++ b/angular/src/components/modal/modal.ref.ts
@@ -0,0 +1,51 @@
+import { Observable, Subject } from 'rxjs';
+import { first } from 'rxjs/operators';
+
+export class ModalRef {
+
+ onCreated: Observable; // Modal added to the DOM.
+ onClose: Observable; // Initiated close.
+ onClosed: Observable; // Modal was closed (Remove element from DOM)
+ onShow: Observable; // Start showing modal
+ onShown: Observable; // Modal is fully visible
+
+ private readonly _onCreated = new Subject();
+ private readonly _onClose = new Subject();
+ private readonly _onClosed = new Subject();
+ private readonly _onShow = new Subject();
+ private readonly _onShown = new Subject();
+ private lastResult: any;
+
+ constructor() {
+ this.onCreated = this._onCreated.asObservable();
+ this.onClose = this._onClose.asObservable();
+ this.onClosed = this._onClosed.asObservable();
+ this.onShow = this._onShow.asObservable();
+ this.onShown = this._onShow.asObservable();
+ }
+
+ show() {
+ this._onShow.next();
+ }
+
+ shown() {
+ this._onShown.next();
+ }
+
+ close(result?: any) {
+ this.lastResult = result;
+ this._onClose.next(result);
+ }
+
+ closed() {
+ this._onClosed.next(this.lastResult);
+ }
+
+ created(el: HTMLElement) {
+ this._onCreated.next(el);
+ }
+
+ onClosedPromise(): Promise {
+ return this.onClosed.pipe(first()).toPromise();
+ }
+}
diff --git a/angular/src/components/password-reprompt.component.ts b/angular/src/components/password-reprompt.component.ts
new file mode 100644
index 0000000000..688453942a
--- /dev/null
+++ b/angular/src/components/password-reprompt.component.ts
@@ -0,0 +1,30 @@
+import { Directive } from '@angular/core';
+
+import { CryptoService } from 'jslib-common/abstractions/crypto.service';
+import { I18nService } from 'jslib-common/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
+import { ModalRef } from './modal/modal.ref';
+
+@Directive()
+export class PasswordRepromptComponent {
+
+ showPassword = false;
+ masterPassword = '';
+
+ constructor(private modalRef: ModalRef, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService) {}
+
+ togglePassword() {
+ this.showPassword = !this.showPassword;
+ }
+
+ async submit() {
+ if (!await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, null)) {
+ this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('invalidMasterPassword'));
+ return;
+ }
+
+ this.modalRef.close(true);
+ }
+}
diff --git a/angular/src/services/modal.service.ts b/angular/src/services/modal.service.ts
new file mode 100644
index 0000000000..f5573b4013
--- /dev/null
+++ b/angular/src/services/modal.service.ts
@@ -0,0 +1,132 @@
+import {
+ ApplicationRef,
+ ComponentFactoryResolver,
+ ComponentRef,
+ EmbeddedViewRef,
+ Injectable,
+ Injector,
+ Type,
+ ViewContainerRef
+} from '@angular/core';
+import { first } from 'rxjs/operators';
+
+import { DynamicModalComponent } from '../components/modal/dynamic-modal.component';
+import { ModalInjector } from '../components/modal/modal-injector';
+import { ModalRef } from '../components/modal/modal.ref';
+
+export class ModalConfig {
+ data?: D;
+ allowMultipleModals: boolean = false;
+}
+
+@Injectable()
+export class ModalService {
+ protected modalCount = 0;
+
+ constructor(private componentFactoryResolver: ComponentFactoryResolver, private applicationRef: ApplicationRef,
+ private injector: Injector) {}
+
+ async openViewRef(componentType: Type, viewContainerRef: ViewContainerRef,
+ setComponentParameters: (component: T) => void = null): Promise<[ModalRef, T]> {
+
+ this.modalCount++;
+ const [modalRef, modalComponentRef] = this.openInternal(componentType, null, false);
+ modalComponentRef.instance.setComponentParameters = setComponentParameters;
+
+ viewContainerRef.insert(modalComponentRef.hostView);
+
+ await modalRef.onCreated.pipe(first()).toPromise();
+
+ return [modalRef, modalComponentRef.instance.componentRef.instance];
+ }
+
+ open(componentType: Type, config?: ModalConfig) {
+ if (!(config?.allowMultipleModals ?? false) && this.modalCount > 0) {
+ return;
+ }
+ this.modalCount++;
+
+ const [modalRef, _] = this.openInternal(componentType, config, true);
+
+ return modalRef;
+ }
+
+ protected openInternal(componentType: Type, config?: ModalConfig, attachToDom?: boolean):
+ [ModalRef, ComponentRef] {
+
+ const [modalRef, componentRef] = this.createModalComponent(config);
+ componentRef.instance.childComponentType = componentType;
+
+ if (attachToDom) {
+ this.applicationRef.attachView(componentRef.hostView);
+ const domElem = (componentRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement;
+ document.body.appendChild(domElem);
+ }
+
+ modalRef.onClosed.pipe(first()).subscribe(() => {
+ if (attachToDom) {
+ this.applicationRef.detachView(componentRef.hostView);
+ }
+ componentRef.destroy();
+ this.modalCount--;
+ });
+
+ this.setupHandlers(modalRef);
+
+ return [modalRef, componentRef];
+ }
+
+ protected setupHandlers(modalRef: ModalRef) {
+ let backdrop: HTMLElement = null;
+
+ // Add backdrop, setup [data-dismiss] handler.
+ modalRef.onCreated.pipe(first()).subscribe(el => {
+ document.body.classList.add('modal-open');
+
+ backdrop = document.createElement('div');
+ backdrop.className = 'modal-backdrop fade';
+ backdrop.style.zIndex = `${this.modalCount}040`;
+ document.body.appendChild(backdrop);
+
+ el.querySelector('.modal-dialog').addEventListener('click', (e: Event) => {
+ e.stopPropagation();
+ });
+
+ const modalEl: HTMLElement = el.querySelector('.modal');
+ modalEl.style.zIndex = `${this.modalCount}050`;
+
+ const modals = Array.from(el.querySelectorAll('.modal, .modal *[data-dismiss="modal"]'));
+ for (const closeElement of modals) {
+ closeElement.addEventListener('click', event => {
+ modalRef.close();
+ });
+ }
+ });
+
+ // onClose is used in Web to hook into bootstrap. On other projects we pipe it directly to closed.
+ modalRef.onClose.pipe(first()).subscribe(() => {
+ modalRef.closed();
+
+ if (this.modalCount === 0) {
+ document.body.classList.remove('modal-open');
+ }
+
+ if (backdrop != null) {
+ document.body.removeChild(backdrop);
+ }
+ });
+ }
+
+ protected createModalComponent(config: ModalConfig): [ModalRef, ComponentRef] {
+ const modalRef = new ModalRef();
+
+ const map = new WeakMap();
+ map.set(ModalConfig, config);
+ map.set(ModalRef, modalRef);
+
+ const componentFactory = this.componentFactoryResolver.resolveComponentFactory(DynamicModalComponent);
+ const componentRef = componentFactory.create(new ModalInjector(this.injector, map));
+
+ return [modalRef, componentRef];
+ }
+}
diff --git a/angular/src/services/passwordReprompt.service.ts b/angular/src/services/passwordReprompt.service.ts
new file mode 100644
index 0000000000..552e558b46
--- /dev/null
+++ b/angular/src/services/passwordReprompt.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+
+import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib-common/abstractions/passwordReprompt.service';
+
+import { PasswordRepromptComponent } from '../components/password-reprompt.component';
+import { ModalService } from './modal.service';
+
+@Injectable()
+export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
+ protected component = PasswordRepromptComponent;
+
+ constructor(private modalService: ModalService) { }
+
+ protectedFields() {
+ return ['TOTP', 'Password', 'H_Field', 'Card Number', 'Security Code'];
+ }
+
+ async showPasswordPrompt() {
+ const ref = this.modalService.open(this.component, {allowMultipleModals: true});
+
+ if (ref == null) {
+ return false;
+ }
+
+ const result = await ref.onClosedPromise();
+ return result === true;
+ }
+}
diff --git a/common/src/abstractions/platformUtils.service.ts b/common/src/abstractions/platformUtils.service.ts
index 264697aa8b..8828d3af7b 100644
--- a/common/src/abstractions/platformUtils.service.ts
+++ b/common/src/abstractions/platformUtils.service.ts
@@ -26,7 +26,6 @@ export abstract class PlatformUtilsService {
options?: any) => void;
showDialog: (body: string, title?: string, confirmText?: string, cancelText?: string,
type?: string, bodyIsHtml?: boolean) => Promise;
- showPasswordDialog: (title: string, body: string, passwordValidation: (value: string) => Promise) => Promise;
isDev: () => boolean;
isSelfHost: () => boolean;
copyToClipboard: (text: string, options?: any) => void | boolean;
diff --git a/common/src/services/passwordReprompt.service.ts b/common/src/services/passwordReprompt.service.ts
deleted file mode 100644
index ae3e1a5120..0000000000
--- a/common/src/services/passwordReprompt.service.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { PlatformUtilsService } from '../abstractions';
-
-import { CryptoService } from '../abstractions/crypto.service';
-import { I18nService } from '../abstractions/i18n.service';
-import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from '../abstractions/passwordReprompt.service';
-
-import { HashPurpose } from '../enums/hashPurpose';
-
-export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
- constructor(private i18nService: I18nService, private cryptoService: CryptoService,
- private platformUtilService: PlatformUtilsService) { }
-
- protectedFields() {
- return ['TOTP', 'Password', 'H_Field', 'Card Number', 'Security Code'];
- }
-
- async showPasswordPrompt() {
- const passwordValidator = (value: string) => {
- return this.cryptoService.compareAndUpdateKeyHash(value, null);
- };
-
- return this.platformUtilService.showPasswordDialog(this.i18nService.t('passwordConfirmation'), this.i18nService.t('passwordConfirmationDesc'), passwordValidator);
- }
-}
diff --git a/electron/src/services/electronPlatformUtils.service.ts b/electron/src/services/electronPlatformUtils.service.ts
index 36da32cc6e..87f719defd 100644
--- a/electron/src/services/electronPlatformUtils.service.ts
+++ b/electron/src/services/electronPlatformUtils.service.ts
@@ -151,11 +151,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return Promise.resolve(result.response === 0);
}
- async showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise):
- Promise {
- throw new Error('Not implemented.');
- }
-
isDev(): boolean {
return isDev();
}
diff --git a/node/src/cli/services/cliPlatformUtils.service.ts b/node/src/cli/services/cliPlatformUtils.service.ts
index e606c6137b..fe2af22c4e 100644
--- a/node/src/cli/services/cliPlatformUtils.service.ts
+++ b/node/src/cli/services/cliPlatformUtils.service.ts
@@ -114,11 +114,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
throw new Error('Not implemented.');
}
- showPasswordDialog(title: string, body: string, passwordValidation: (value: string) => Promise):
- Promise {
- throw new Error('Not implemented.');
- }
-
isDev(): boolean {
return process.env.BWCLI_ENV === 'development';
}