mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-13 19:51:37 +01:00
[PM-4816] Create shared LoginApprovalComponent (#11982)
* Stub out dialog * Genericize LoginApprovalComponent * update ipc mocks * Remove changes to account component * Remove changes to account component * Remove debug * Remove test component * Remove added translations * Fix failing test * Run lint and prettier * Rename LoginApprovalServiceAbstraction to LoginApprovalComponentServiceAbstraction * Add back missing "isVisible" check before calling loginRequest * Rename classes to contain "Component" in the name * Add missing space between "login attempt" and fingerprint phrase * Require email
This commit is contained in:
parent
13d4b6f2a6
commit
02ea368446
@ -25,7 +25,7 @@ import {
|
|||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||||
import { LogoutReason } from "@bitwarden/auth/common";
|
import { LogoutReason } from "@bitwarden/auth/common";
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
@ -67,7 +67,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
|||||||
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||||
import { LoginApprovalComponent } from "../auth/login/login-approval.component";
|
|
||||||
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
||||||
import { flagEnabled } from "../platform/flags";
|
import { flagEnabled } from "../platform/flags";
|
||||||
import { PremiumComponent } from "../vault/app/accounts/premium.component";
|
import { PremiumComponent } from "../vault/app/accounts/premium.component";
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import {
|
import {
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
LoginApprovalComponentServiceAbstraction,
|
||||||
LoginEmailService,
|
LoginEmailService,
|
||||||
PinServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
@ -87,6 +88,7 @@ import {
|
|||||||
BiometricsService,
|
BiometricsService,
|
||||||
} from "@bitwarden/key-management";
|
} from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||||
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
||||||
@ -349,6 +351,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: LoginEmailService,
|
useClass: LoginEmailService,
|
||||||
deps: [AccountService, AuthService, StateProvider],
|
deps: [AccountService, AuthService, StateProvider],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LoginApprovalComponentServiceAbstraction,
|
||||||
|
useClass: DesktopLoginApprovalComponentService,
|
||||||
|
deps: [I18nServiceAbstraction],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||||
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { DesktopLoginApprovalComponentService } from "./desktop-login-approval-component.service";
|
||||||
|
|
||||||
|
describe("DesktopLoginApprovalComponentService", () => {
|
||||||
|
let service: DesktopLoginApprovalComponentService;
|
||||||
|
let i18nService: MockProxy<I18nServiceAbstraction>;
|
||||||
|
let originalIpc: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalIpc = (global as any).ipc;
|
||||||
|
(global as any).ipc = {
|
||||||
|
auth: {
|
||||||
|
loginRequest: jest.fn(),
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
isWindowVisible: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
i18nService = mock<I18nServiceAbstraction>({
|
||||||
|
t: jest.fn(),
|
||||||
|
userSetLocale$: new Subject<string>(),
|
||||||
|
locale$: new Subject<string>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
DesktopLoginApprovalComponentService,
|
||||||
|
{ provide: I18nServiceAbstraction, useValue: i18nService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(DesktopLoginApprovalComponentService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(global as any).ipc = originalIpc;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is created successfully", () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls ipc.auth.loginRequest with correct parameters when window is not visible", async () => {
|
||||||
|
const title = "Log in requested";
|
||||||
|
const email = "test@bitwarden.com";
|
||||||
|
const message = `Confirm login attempt for ${email}`;
|
||||||
|
const closeText = "Close";
|
||||||
|
|
||||||
|
const loginApprovalComponent = { email } as LoginApprovalComponent;
|
||||||
|
i18nService.t.mockImplementation((key: string) => {
|
||||||
|
switch (key) {
|
||||||
|
case "logInRequested":
|
||||||
|
return title;
|
||||||
|
case "confirmLoginAtemptForMail":
|
||||||
|
return message;
|
||||||
|
case "close":
|
||||||
|
return closeText;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(false);
|
||||||
|
jest.spyOn(ipc.auth, "loginRequest").mockResolvedValue();
|
||||||
|
|
||||||
|
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
|
||||||
|
|
||||||
|
expect(ipc.auth.loginRequest).toHaveBeenCalledWith(title, message, closeText);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call ipc.auth.loginRequest when window is visible", async () => {
|
||||||
|
const loginApprovalComponent = { email: "test@bitwarden.com" } as LoginApprovalComponent;
|
||||||
|
|
||||||
|
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(true);
|
||||||
|
jest.spyOn(ipc.auth, "loginRequest");
|
||||||
|
|
||||||
|
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
|
||||||
|
|
||||||
|
expect(ipc.auth.loginRequest).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular";
|
||||||
|
import { LoginApprovalComponentServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DesktopLoginApprovalComponentService
|
||||||
|
extends DefaultLoginApprovalComponentService
|
||||||
|
implements LoginApprovalComponentServiceAbstraction
|
||||||
|
{
|
||||||
|
constructor(private i18nService: I18nServiceAbstraction) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async showLoginRequestedAlertIfWindowNotVisible(email: string): Promise<void> {
|
||||||
|
const isVisible = await ipc.platform.isWindowVisible();
|
||||||
|
if (!isVisible) {
|
||||||
|
await ipc.auth.loginRequest(
|
||||||
|
this.i18nService.t("logInRequested"),
|
||||||
|
this.i18nService.t("confirmLoginAtemptForMail", email),
|
||||||
|
this.i18nService.t("close"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -66,3 +66,7 @@ export * from "./vault-timeout-input/vault-timeout-input.component";
|
|||||||
|
|
||||||
// self hosted environment configuration dialog
|
// self hosted environment configuration dialog
|
||||||
export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.component";
|
export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.component";
|
||||||
|
|
||||||
|
// login approval
|
||||||
|
export * from "./login-approval/login-approval.component";
|
||||||
|
export * from "./login-approval/default-login-approval-component.service";
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { DefaultLoginApprovalComponentService } from "./default-login-approval-component.service";
|
||||||
|
import { LoginApprovalComponent } from "./login-approval.component";
|
||||||
|
|
||||||
|
describe("DefaultLoginApprovalComponentService", () => {
|
||||||
|
let service: DefaultLoginApprovalComponentService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [DefaultLoginApprovalComponentService],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(DefaultLoginApprovalComponentService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is created successfully", () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has showLoginRequestedAlertIfWindowNotVisible method that is a no-op", async () => {
|
||||||
|
const loginApprovalComponent = {} as LoginApprovalComponent;
|
||||||
|
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,16 @@
|
|||||||
|
import { LoginApprovalComponentServiceAbstraction } from "../../common/abstractions/login-approval-component.service.abstraction";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of the LoginApprovalComponentServiceAbstraction.
|
||||||
|
*/
|
||||||
|
export class DefaultLoginApprovalComponentService
|
||||||
|
implements LoginApprovalComponentServiceAbstraction
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<bit-dialog>
|
<bit-dialog>
|
||||||
<span bitDialogTitle>{{ "areYouTryingtoLogin" | i18n }}</span>
|
<span bitDialogTitle>{{ "areYouTryingtoLogin" | i18n }}</span>
|
||||||
<ng-container bitDialogContent>
|
<ng-container bitDialogContent>
|
||||||
<h4>{{ "logInAttemptBy" | i18n: email }}</h4>
|
<h4 class="tw-mb-3">{{ "logInAttemptBy" | i18n: email }}</h4>
|
||||||
<div>
|
<div>
|
||||||
<b>{{ "fingerprintPhraseHeader" | i18n }}</b>
|
<b>{{ "fingerprintPhraseHeader" | i18n }}</b>
|
||||||
<p class="tw-text-code">{{ fingerprintPhrase }}</p>
|
<p class="tw-text-code">{{ fingerprintPhrase }}</p>
|
@ -0,0 +1,122 @@
|
|||||||
|
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthRequestServiceAbstraction,
|
||||||
|
LoginApprovalComponentServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { LoginApprovalComponent } from "./login-approval.component";
|
||||||
|
|
||||||
|
describe("LoginApprovalComponent", () => {
|
||||||
|
let component: LoginApprovalComponent;
|
||||||
|
let fixture: ComponentFixture<LoginApprovalComponent>;
|
||||||
|
|
||||||
|
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||||
|
let accountService: MockProxy<AccountService>;
|
||||||
|
let apiService: MockProxy<ApiService>;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let dialogRef: MockProxy<DialogRef>;
|
||||||
|
let toastService: MockProxy<ToastService>;
|
||||||
|
|
||||||
|
const testNotificationId = "test-notification-id";
|
||||||
|
const testEmail = "test@bitwarden.com";
|
||||||
|
const testPublicKey = "test-public-key";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||||
|
accountService = mock<AccountService>();
|
||||||
|
apiService = mock<ApiService>();
|
||||||
|
i18nService = mock<I18nService>();
|
||||||
|
dialogRef = mock<DialogRef>();
|
||||||
|
toastService = mock<ToastService>();
|
||||||
|
|
||||||
|
accountService.activeAccount$ = of({
|
||||||
|
email: testEmail,
|
||||||
|
id: "test-user-id" as UserId,
|
||||||
|
emailVerified: true,
|
||||||
|
name: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginApprovalComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: DIALOG_DATA, useValue: { notificationId: testNotificationId } },
|
||||||
|
{ provide: AuthRequestServiceAbstraction, useValue: authRequestService },
|
||||||
|
{ provide: AccountService, useValue: accountService },
|
||||||
|
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||||
|
{ provide: I18nService, useValue: i18nService },
|
||||||
|
{ provide: ApiService, useValue: apiService },
|
||||||
|
{ provide: AppIdService, useValue: mock<AppIdService>() },
|
||||||
|
{ provide: KeyService, useValue: mock<KeyService>() },
|
||||||
|
{ provide: DialogRef, useValue: dialogRef },
|
||||||
|
{ provide: ToastService, useValue: toastService },
|
||||||
|
{
|
||||||
|
provide: LoginApprovalComponentServiceAbstraction,
|
||||||
|
useValue: mock<LoginApprovalComponentServiceAbstraction>(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginApprovalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates successfully", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ngOnInit", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apiService.getAuthRequest.mockResolvedValue({
|
||||||
|
publicKey: testPublicKey,
|
||||||
|
creationDate: new Date().toISOString(),
|
||||||
|
} as AuthRequestResponse);
|
||||||
|
authRequestService.getFingerprintPhrase.mockResolvedValue("test-phrase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retrieves and sets auth request data", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
|
||||||
|
expect(apiService.getAuthRequest).toHaveBeenCalledWith(testNotificationId);
|
||||||
|
expect(component.email).toBe(testEmail);
|
||||||
|
expect(component.fingerprintPhrase).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates time text initially", async () => {
|
||||||
|
i18nService.t.mockReturnValue("justNow");
|
||||||
|
|
||||||
|
await component.ngOnInit();
|
||||||
|
expect(component.requestTimeText).toBe("justNow");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("denyLogin", () => {
|
||||||
|
it("denies auth request and shows info toast", async () => {
|
||||||
|
const response = { requestApproved: false } as AuthRequestResponse;
|
||||||
|
apiService.getAuthRequest.mockResolvedValue(response);
|
||||||
|
authRequestService.approveOrDenyAuthRequest.mockResolvedValue(response);
|
||||||
|
i18nService.t.mockReturnValue("denied message");
|
||||||
|
|
||||||
|
await component.denyLogin();
|
||||||
|
|
||||||
|
expect(authRequestService.approveOrDenyAuthRequest).toHaveBeenCalledWith(false, response);
|
||||||
|
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "info",
|
||||||
|
title: null,
|
||||||
|
message: "denied message",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -4,7 +4,10 @@ import { Component, OnInit, OnDestroy, Inject } from "@angular/core";
|
|||||||
import { Subject, firstValueFrom, map } from "rxjs";
|
import { Subject, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
import {
|
||||||
|
AuthRequestServiceAbstraction,
|
||||||
|
LoginApprovalComponentServiceAbstraction as LoginApprovalComponentService,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
@ -56,6 +59,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
|
|||||||
protected keyService: KeyService,
|
protected keyService: KeyService,
|
||||||
private dialogRef: DialogRef,
|
private dialogRef: DialogRef,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
|
private loginApprovalComponentService: LoginApprovalComponentService,
|
||||||
) {
|
) {
|
||||||
this.notificationId = params.notificationId;
|
this.notificationId = params.notificationId;
|
||||||
}
|
}
|
||||||
@ -89,14 +93,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
|
|||||||
this.updateTimeText();
|
this.updateTimeText();
|
||||||
}, RequestTimeUpdate);
|
}, RequestTimeUpdate);
|
||||||
|
|
||||||
const isVisible = await ipc.platform.isWindowVisible();
|
this.loginApprovalComponentService.showLoginRequestedAlertIfWindowNotVisible(this.email);
|
||||||
if (!isVisible) {
|
|
||||||
await ipc.auth.loginRequest(
|
|
||||||
this.i18nService.t("logInRequested"),
|
|
||||||
this.i18nService.t("confirmLoginAtemptForMail", this.email),
|
|
||||||
this.i18nService.t("close"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,3 +4,4 @@ export * from "./login-email.service";
|
|||||||
export * from "./login-strategy.service";
|
export * from "./login-strategy.service";
|
||||||
export * from "./user-decryption-options.service.abstraction";
|
export * from "./user-decryption-options.service.abstraction";
|
||||||
export * from "./auth-request.service.abstraction";
|
export * from "./auth-request.service.abstraction";
|
||||||
|
export * from "./login-approval-component.service.abstraction";
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Abstraction for the LoginApprovalComponent service.
|
||||||
|
*/
|
||||||
|
export abstract class LoginApprovalComponentServiceAbstraction {
|
||||||
|
/**
|
||||||
|
* Shows a login requested alert if the window is not visible.
|
||||||
|
*/
|
||||||
|
abstract showLoginRequestedAlertIfWindowNotVisible: (email?: string) => Promise<void>;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user