1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-29 22:31:29 +01:00

Merge branch 'main' into PM-12036

This commit is contained in:
cd-bitwarden 2025-01-28 17:34:49 -05:00 committed by GitHub
commit ced9b7c137
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
174 changed files with 1508 additions and 830 deletions

View File

@ -1,29 +0,0 @@
**/build
**/dist
**/coverage
.angular
storybook-static
**/node_modules
**/webpack.*.js
**/jest.config.js
apps/browser/config/config.js
apps/browser/src/auth/scripts/duo.js
apps/browser/webpack/manifest.js
apps/desktop/desktop_native
apps/desktop/src/auth/scripts/duo.js
apps/web/config.js
apps/web/scripts/*.js
apps/web/tailwind.config.js
apps/cli/config/config.js
tailwind.config.js
libs/components/tailwind.config.base.js
libs/components/tailwind.config.js
scripts/*.js

View File

@ -1,258 +0,0 @@
{
"root": true,
"env": {
"browser": true,
"webextensions": true
},
"overrides": [
{
"files": ["*.ts", "*.js"],
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.eslint.json"],
"sourceType": "module",
"ecmaVersion": 2020
},
"extends": [
"eslint:recommended",
"plugin:@angular-eslint/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:rxjs/recommended",
"prettier",
"plugin:storybook/recommended"
],
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts"]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
}
}
},
"rules": {
"@angular-eslint/component-class-suffix": 0,
"@angular-eslint/contextual-lifecycle": 0,
"@angular-eslint/directive-class-suffix": 0,
"@angular-eslint/no-empty-lifecycle-method": 0,
"@angular-eslint/no-host-metadata-property": 0,
"@angular-eslint/no-input-rename": 0,
"@angular-eslint/no-inputs-metadata-property": 0,
"@angular-eslint/no-output-native": 0,
"@angular-eslint/no-output-on-prefix": 0,
"@angular-eslint/no-output-rename": 0,
"@angular-eslint/no-outputs-metadata-property": 0,
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": 0,
"@typescript-eslint/explicit-member-accessibility": [
"error",
{ "accessibility": "no-public" }
],
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }],
"@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }],
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"no-console": "error",
"import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package.
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "@bitwarden/**",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
],
"rxjs-angular/prefer-takeuntil": ["error", { "alias": ["takeUntilDestroyed"] }],
"rxjs/no-exposed-subjects": ["error", { "allowProtected": true }],
"no-restricted-syntax": [
"error",
{
"message": "Calling `svgIcon` directly is not allowed",
"selector": "CallExpression[callee.name='svgIcon']"
},
{
"message": "Accessing FormGroup using `get` is not allowed, use `.value` instead",
"selector": "ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']"
}
],
"curly": ["error", "all"],
"import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"target": ["libs/**/*"],
"from": ["apps/**/*"],
"message": "Libs should not import app-specific code."
},
{
// avoid specific frameworks or large dependencies in common
"target": "./libs/common/**/*",
"from": [
// Angular
"./libs/angular/**/*",
"./node_modules/@angular*/**/*",
// Node
"./libs/node/**/*",
//Generator
"./libs/tools/generator/components/**/*",
"./libs/tools/generator/core/**/*",
"./libs/tools/generator/extensions/**/*",
// Import/export
"./libs/importer/**/*",
"./libs/tools/export/vault-export/vault-export-core/**/*"
]
},
{
// avoid import of unexported state objects
"target": [
"!(libs)/**/*",
"libs/!(common)/**/*",
"libs/common/!(src)/**/*",
"libs/common/src/!(platform)/**/*",
"libs/common/src/platform/!(state)/**/*"
],
"from": ["./libs/common/src/platform/state/**/*"],
// allow module index import
"except": ["**/state/index.ts"]
}
]
}
]
}
},
{
"files": ["*.html"],
"parser": "@angular-eslint/template-parser",
"plugins": ["@angular-eslint/template", "tailwindcss"],
"rules": {
"@angular-eslint/template/button-has-type": "error",
"tailwindcss/no-custom-classname": [
"error",
{
// uses negative lookahead to whitelist any class that doesn't start with "tw-"
// in other words: classnames that start with tw- must be valid TailwindCSS classes
"whitelist": ["(?!(tw)\\-).*"]
}
],
"tailwindcss/enforces-negative-arbitrary-values": "error",
"tailwindcss/enforces-shorthand": "error",
"tailwindcss/no-contradicting-classname": "error"
}
},
{
"files": ["apps/browser/src/**/*.ts", "libs/**/*.ts"],
"excludedFiles": [
"apps/browser/src/autofill/{content,notification}/**/*.ts",
"apps/browser/src/**/background/**/*.ts", // It's okay to have long lived listeners in the background
"apps/browser/src/platform/background.ts"
],
"rules": {
"no-restricted-syntax": [
"error",
{
"message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead",
// This selector covers events like chrome.storage.onChange & chrome.runtime.onMessage
"selector": "CallExpression > [object.object.object.name='chrome'][property.name='addListener']"
},
{
"message": "Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi.addListener` instead",
// This selector covers events like chrome.storage.local.onChange
"selector": "CallExpression > [object.object.object.object.name='chrome'][property.name='addListener']"
}
]
}
},
{
"files": ["**/*.ts"],
"excludedFiles": ["**/platform/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
"**/platform/**/internal", // General internal pattern
// All features that have been converted to barrel files
"**/platform/messaging/**"
]
}
]
}
},
{
"files": ["**/src/**/*.ts"],
"excludedFiles": ["**/platform/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
"**/platform/**/internal", // General internal pattern
// All features that have been converted to barrel files
"**/platform/messaging/**",
"**/src/**/*" // Prevent relative imports across libs.
]
}
]
}
},
{
"files": ["bitwarden_license/bit-common/src/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{ "patterns": ["@bitwarden/bit-common/*", "**/src/**/*"] }
]
}
},
{
"files": ["apps/**/*.ts"],
"rules": {
// Catches static imports
"no-restricted-imports": [
"error",
{
"patterns": [
"biwarden_license/**",
"@bitwarden/bit-common/*",
"@bitwarden/bit-web/*",
"**/src/**/*"
]
}
],
// Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded
"no-restricted-syntax": [
"error",
{
"message": "Don't import Bitwarden licensed code into OSS code.",
"selector": "ImportExpression > Literal.source[value=/.*(bitwarden_license|bit-common|bit-web).*/]"
}
]
}
}
]
}

25
.github/renovate.json vendored
View File

@ -69,6 +69,26 @@
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
},
{
"matchPackageNames": [
"@angular-eslint/schematics",
"angular-eslint",
"eslint-config-prettier",
"eslint-import-resolver-typescript",
"eslint-plugin-import",
"eslint-plugin-rxjs-angular",
"eslint-plugin-rxjs",
"eslint-plugin-storybook",
"eslint-plugin-tailwindcss",
"eslint",
"husky",
"lint-staged",
"typescript-eslint"
],
"description": "Architecture owned dependencies",
"commitMessagePrefix": "[deps] Architecture:",
"reviewers": ["team:dept-architecture"]
},
{
"matchPackageNames": [
"@angular-eslint/eslint-plugin-template",
@ -88,9 +108,8 @@
"husky",
"lint-staged"
],
"description": "Architecture owned dependencies",
"commitMessagePrefix": "[deps] Architecture:",
"reviewers": ["team:dept-architecture"]
"groupName": "Linting minor-patch",
"matchUpdateTypes": ["minor", "patch"]
},
{
"matchPackageNames": [

View File

@ -1,7 +1,8 @@
import { dirname, join } from "path";
import { StorybookConfig } from "@storybook/angular";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
import remarkGfm from "remark-gfm";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
const config: StorybookConfig = {
stories: [
@ -29,6 +30,8 @@ const config: StorybookConfig = {
getAbsolutePath("@storybook/addon-designs"),
getAbsolutePath("@storybook/addon-interactions"),
{
// @storybook/addon-docs is part of @storybook/addon-essentials
// eslint-disable-next-line storybook/no-uninstalled-addons
name: "@storybook/addon-docs",
options: {
mdxPluginOptions: {

View File

@ -50,10 +50,14 @@ const darkTheme = create({
});
export const getPreferredColorScheme = () => {
if (!globalThis || !globalThis.matchMedia) return "light";
if (!globalThis || !globalThis.matchMedia) {
return "light";
}
const isDarkThemePreferred = globalThis.matchMedia("(prefers-color-scheme: dark)").matches;
if (isDarkThemePreferred) return "dark";
if (isDarkThemePreferred) {
return "dark";
}
return "light";
};

View File

@ -7,5 +7,6 @@
"**/_locales/*[^n]/messages.json": true
},
"rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"],
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.useFlatConfig": true
}

View File

@ -1,26 +0,0 @@
{
"env": {
"browser": true,
"webextensions": true
},
"overrides": [
{
"files": ["src/**/*.ts"],
"excludedFiles": [
"src/**/{content,popup,spec}/**/*.ts",
"src/**/autofill/{notification,overlay}/**/*.ts",
"src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts",
"src/**/*.spec.ts"
],
"rules": {
"no-restricted-globals": [
"error",
{
"name": "window",
"message": "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead."
}
]
}
}
]
}

View File

@ -13,9 +13,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService } from "@bitwarden/key-management";
@Component({
@ -34,9 +32,7 @@ export class RegisterComponent extends BaseRegisterComponent {
i18nService: I18nService,
keyService: KeyService,
apiService: ApiService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
environmentService: EnvironmentService,
logService: LogService,
auditService: AuditService,
@ -51,9 +47,7 @@ export class RegisterComponent extends BaseRegisterComponent {
i18nService,
keyService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService,
auditService,

View File

@ -1,8 +1,8 @@
import { dirname, join } from "path";
import path from "path";
import path, { dirname, join } from "path";
import type { StorybookConfig } from "@storybook/web-components-webpack5";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
import remarkGfm from "remark-gfm";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
const getAbsolutePath = (value: string): string =>
dirname(require.resolve(join(value, "package.json")));

View File

@ -47,6 +47,6 @@
<div
*ngIf="showOverlay"
class="tw-absolute tw-w-full tw-h-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
class="tw-absolute tw-size-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
></div>
</ng-container>

View File

@ -13,13 +13,13 @@
<ng-content select="[slot=above-scroll-area]"></ng-content>
</div>
<div
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full tw-styled-scrollbar"
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-size-full tw-styled-scrollbar"
data-testid="popup-layout-scroll-region"
(scroll)="handleScroll($event)"
[ngClass]="{ 'tw-invisible': loading }"
>
<div
class="tw-max-w-screen-sm tw-mx-auto tw-flex-1 tw-flex tw-flex-col tw-h-full tw-w-full"
class="tw-max-w-screen-sm tw-mx-auto tw-flex-1 tw-flex tw-flex-col tw-size-full"
[ngClass]="{ 'tw-p-3 bit-compact:tw-p-2': !disablePadding }"
>
<ng-content></ng-content>

View File

@ -11,11 +11,11 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { DialogService, ItemModule } from "@bitwarden/components";
import { FamiliesPolicyService } from "../../../../billing/services/families-policy.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { FamiliesPolicyService } from "../../../../services/families-policy.service";
@Component({
templateUrl: "more-from-bitwarden-page-v2.component.html",

View File

@ -20,7 +20,7 @@
</p>
<div
*ngIf="showBadge$ | async"
class="tw-flex tw-items-center tw-justify-center tw-z-10 tw-absolute tw-rounded-full tw-h-[15px] tw-w-[15px] tw-top-[1px] tw-right-[1px] tw-text-notification-600 tw-text-[8px] tw-border-notification-600 tw-border-[0.5px] tw-border-solid tw-bg-notification-100 tw-leading-normal"
class="tw-flex tw-items-center tw-justify-center tw-z-10 tw-absolute tw-rounded-full tw-size-[15px] tw-top-[1px] tw-right-[1px] tw-text-notification-600 tw-text-[8px] tw-border-notification-600 tw-border-[0.5px] tw-border-solid tw-bg-notification-100 tw-leading-normal"
data-testid="filter-badge"
>
{{ numberOfAppliedFilters$ | async }}

View File

@ -2,8 +2,9 @@
<form
[formGroup]="filterForm"
class="tw-gap-2 tw-mt-2 tw-grid tw-grid-cols-2 sm:tw-grid-cols-3 lg:tw-grid-cols-4"
*ngIf="allFilters$ | async as allFilters"
>
<ng-container *ngIf="organizations$ | async as organizations">
<ng-container *ngIf="allFilters.organizations as organizations">
<bit-chip-select
*ngIf="organizations.length"
fullWidth
@ -14,7 +15,7 @@
>
</bit-chip-select>
</ng-container>
<ng-container *ngIf="collections$ | async as collections">
<ng-container *ngIf="allFilters.collections as collections">
<bit-chip-select
*ngIf="collections.length"
fullWidth
@ -25,7 +26,7 @@
>
</bit-chip-select>
</ng-container>
<ng-container *ngIf="folders$ | async as folders">
<ng-container *ngIf="allFilters.folders as folders">
<bit-chip-select
*ngIf="folders.length"
fullWidth

View File

@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { ReactiveFormsModule } from "@angular/forms";
import { combineLatest, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ChipSelectComponent } from "@bitwarden/components";
@ -20,5 +21,20 @@ export class VaultListFiltersComponent {
protected folders$ = this.vaultPopupListFiltersService.folders$;
protected cipherTypes = this.vaultPopupListFiltersService.cipherTypes;
// Combine all filters into a single observable to eliminate the filters from loading separately in the UI.
protected allFilters$ = combineLatest([
this.organizations$,
this.collections$,
this.folders$,
]).pipe(
map(([organizations, collections, folders]) => {
return {
organizations,
collections,
folders,
};
}),
);
constructor(private vaultPopupListFiltersService: VaultPopupListFiltersService) {}
}

View File

@ -1,10 +1,9 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CdkVirtualScrollableElement, ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnDestroy, OnInit } from "@angular/core";
import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { RouterLink } from "@angular/router";
import { combineLatest, Observable, shareReplay, switchMap } from "rxjs";
import { filter, map, take } from "rxjs/operators";
import { combineLatest, filter, map, Observable, shareReplay, switchMap, take } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
@ -19,6 +18,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service";
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import {
@ -58,7 +58,9 @@ enum VaultState {
DecryptionFailureDialogComponent,
],
})
export class VaultV2Component implements OnInit, OnDestroy {
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
cipherType = CipherType;
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
@ -88,9 +90,12 @@ export class VaultV2Component implements OnInit, OnDestroy {
protected VaultStateEnum = VaultState;
private allFilters$ = this.vaultPopupListFiltersService.allFilters$;
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
private vaultPopupListFiltersService: VaultPopupListFiltersService,
private vaultScrollPositionService: VaultPopupScrollPositionService,
private destroyRef: DestroyRef,
private cipherService: CipherService,
private dialogService: DialogService,
@ -119,6 +124,17 @@ export class VaultV2Component implements OnInit, OnDestroy {
});
}
ngAfterViewInit(): void {
if (this.virtualScrollElement) {
// The filters component can cause the size of the virtual scroll element to change,
// which can cause the scroll position to be land in the wrong spot. To fix this,
// wait until all filters are populated before restoring the scroll position.
this.allFilters$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.vaultScrollPositionService.start(this.virtualScrollElement!);
});
}
}
async ngOnInit() {
this.cipherService.failedToDecryptCiphers$
.pipe(
@ -134,5 +150,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
});
}
ngOnDestroy(): void {}
ngOnDestroy(): void {
this.vaultScrollPositionService.stop();
}
}

View File

@ -21,12 +21,15 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { CopyCipherFieldService } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service";
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
import { ViewV2Component } from "./view-v2.component";
@ -44,6 +47,10 @@ describe("ViewV2Component", () => {
const collect = jest.fn().mockResolvedValue(null);
const doAutofill = jest.fn().mockResolvedValue(true);
const copy = jest.fn().mockResolvedValue(true);
const back = jest.fn().mockResolvedValue(null);
const openSimpleDialog = jest.fn().mockResolvedValue(true);
const stop = jest.fn();
const showToast = jest.fn();
const mockCipher = {
id: "122-333-444",
@ -54,7 +61,7 @@ describe("ViewV2Component", () => {
password: "test-password",
totp: "123",
},
};
} as unknown as CipherView;
const mockVaultPopupAutofillService = {
doAutofill,
@ -68,13 +75,21 @@ describe("ViewV2Component", () => {
const mockCipherService = {
get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }),
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
deleteWithServer: jest.fn().mockResolvedValue(undefined),
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
};
beforeEach(async () => {
mockCipherService.deleteWithServer.mockClear();
mockCipherService.softDeleteWithServer.mockClear();
mockNavigate.mockClear();
collect.mockClear();
doAutofill.mockClear();
copy.mockClear();
stop.mockClear();
openSimpleDialog.mockClear();
back.mockClear();
showToast.mockClear();
await TestBed.configureTestingModule({
imports: [ViewV2Component],
@ -84,9 +99,12 @@ describe("ViewV2Component", () => {
{ provide: LogService, useValue: mock<LogService>() },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>({ back }) },
{ provide: ActivatedRoute, useValue: { queryParams: params$ } },
{ provide: EventCollectionService, useValue: { collect } },
{ provide: VaultPopupScrollPositionService, useValue: { stop } },
{ provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService },
{ provide: ToastService, useValue: { showToast } },
{
provide: I18nService,
useValue: {
@ -98,7 +116,6 @@ describe("ViewV2Component", () => {
},
},
},
{ provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService },
{
provide: AccountService,
useValue: accountService,
@ -114,7 +131,13 @@ describe("ViewV2Component", () => {
useValue: mockCopyCipherFieldService,
},
],
}).compileComponents();
})
.overrideProvider(DialogService, {
useValue: {
openSimpleDialog,
},
})
.compileComponents();
fixture = TestBed.createComponent(ViewV2Component);
component = fixture.componentInstance;
@ -223,4 +246,130 @@ describe("ViewV2Component", () => {
expect(closeSpy).toHaveBeenCalledTimes(1);
}));
});
describe("delete", () => {
beforeEach(() => {
component.cipher = mockCipher;
});
it("opens confirmation modal", async () => {
await component.delete();
expect(openSimpleDialog).toHaveBeenCalledTimes(1);
});
it("navigates back", async () => {
await component.delete();
expect(back).toHaveBeenCalledTimes(1);
});
it("stops scroll position service", async () => {
await component.delete();
expect(stop).toHaveBeenCalledTimes(1);
expect(stop).toHaveBeenCalledWith(true);
});
describe("deny confirmation", () => {
beforeEach(() => {
openSimpleDialog.mockResolvedValue(false);
});
it("does not delete the cipher", async () => {
await component.delete();
expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled();
expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled();
});
it("does not interact with side effects", () => {
expect(back).not.toHaveBeenCalled();
expect(stop).not.toHaveBeenCalled();
expect(showToast).not.toHaveBeenCalled();
});
});
describe("accept confirmation", () => {
beforeEach(() => {
openSimpleDialog.mockResolvedValue(true);
});
describe("soft delete", () => {
beforeEach(() => {
(mockCipher as any).isDeleted = null;
});
it("opens confirmation dialog", async () => {
await component.delete();
expect(openSimpleDialog).toHaveBeenCalledTimes(1);
expect(openSimpleDialog).toHaveBeenCalledWith({
content: {
key: "deleteItemConfirmation",
},
title: {
key: "deleteItem",
},
type: "warning",
});
});
it("calls soft delete", async () => {
await component.delete();
expect(mockCipherService.softDeleteWithServer).toHaveBeenCalled();
expect(mockCipherService.deleteWithServer).not.toHaveBeenCalled();
});
it("shows toast", async () => {
await component.delete();
expect(showToast).toHaveBeenCalledWith({
variant: "success",
title: null,
message: "deletedItem",
});
});
});
describe("hard delete", () => {
beforeEach(() => {
(mockCipher as any).isDeleted = true;
});
it("opens confirmation dialog", async () => {
await component.delete();
expect(openSimpleDialog).toHaveBeenCalledTimes(1);
expect(openSimpleDialog).toHaveBeenCalledWith({
content: {
key: "permanentlyDeleteItemConfirmation",
},
title: {
key: "deleteItem",
},
type: "warning",
});
});
it("calls soft delete", async () => {
await component.delete();
expect(mockCipherService.deleteWithServer).toHaveBeenCalled();
expect(mockCipherService.softDeleteWithServer).not.toHaveBeenCalled();
});
it("shows toast", async () => {
await component.delete();
expect(showToast).toHaveBeenCalledWith({
variant: "success",
title: null,
message: "permanentlyDeletedItem",
});
});
});
});
});
});

View File

@ -49,6 +49,7 @@ import { PopOutComponent } from "../../../../../platform/popup/components/pop-ou
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service";
import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service";
import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window";
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
@ -113,6 +114,7 @@ export class ViewV2Component {
private popupRouterCacheService: PopupRouterCacheService,
protected cipherAuthorizationService: CipherAuthorizationService,
private copyCipherFieldService: CopyCipherFieldService,
private popupScrollPositionService: VaultPopupScrollPositionService,
) {
this.subscribeToParams();
}
@ -202,6 +204,7 @@ export class ViewV2Component {
return false;
}
this.popupScrollPositionService.stop(true);
await this.popupRouterCacheService.back();
this.toastService.showToast({

View File

@ -370,6 +370,9 @@ export class VaultPopupListFiltersService {
),
);
/** Organizations, collection, folders filters. */
allFilters$ = combineLatest([this.organizations$, this.collections$, this.folders$]);
/** Updates the stored state for filter visibility. */
async updateFilterVisibility(isVisible: boolean): Promise<void> {
await this.filterVisibilityState.update(() => isVisible);

View File

@ -0,0 +1,137 @@
import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling";
import { fakeAsync, TestBed, tick } from "@angular/core/testing";
import { NavigationEnd, Router } from "@angular/router";
import { Subject, Subscription } from "rxjs";
import { VaultPopupScrollPositionService } from "./vault-popup-scroll-position.service";
describe("VaultPopupScrollPositionService", () => {
let service: VaultPopupScrollPositionService;
const events$ = new Subject();
const unsubscribe = jest.fn();
beforeEach(async () => {
unsubscribe.mockClear();
await TestBed.configureTestingModule({
providers: [
VaultPopupScrollPositionService,
{ provide: Router, useValue: { events: events$ } },
],
});
service = TestBed.inject(VaultPopupScrollPositionService);
// set up dummy values
service["scrollPosition"] = 234;
service["scrollSubscription"] = { unsubscribe } as unknown as Subscription;
});
describe("router events", () => {
it("does not reset service when navigating to `/tabs/vault`", fakeAsync(() => {
const event = new NavigationEnd(22, "/tabs/vault", "");
events$.next(event);
tick();
expect(service["scrollPosition"]).toBe(234);
expect(service["scrollSubscription"]).not.toBeNull();
}));
it("resets values when navigating to other tab pages", fakeAsync(() => {
const event = new NavigationEnd(23, "/tabs/generator", "");
events$.next(event);
tick();
expect(service["scrollPosition"]).toBeNull();
expect(unsubscribe).toHaveBeenCalled();
expect(service["scrollSubscription"]).toBeNull();
}));
});
describe("stop", () => {
it("removes scroll listener", () => {
service.stop();
expect(unsubscribe).toHaveBeenCalledTimes(1);
expect(service["scrollSubscription"]).toBeNull();
});
it("resets stored values", () => {
service.stop(true);
expect(service["scrollPosition"]).toBeNull();
});
});
describe("start", () => {
const elementScrolled$ = new Subject();
const focus = jest.fn();
const nativeElement = {
scrollTop: 0,
querySelector: jest.fn(() => ({ focus })),
addEventListener: jest.fn(),
style: {
visibility: "",
},
};
const virtualElement = {
elementScrolled: () => elementScrolled$,
getElementRef: () => ({ nativeElement }),
scrollTo: jest.fn(),
} as unknown as CdkVirtualScrollableElement;
afterEach(() => {
// remove the actual subscription created by `.subscribe`
service["scrollSubscription"]?.unsubscribe();
});
describe("initial scroll position", () => {
beforeEach(() => {
(virtualElement.scrollTo as jest.Mock).mockClear();
nativeElement.querySelector.mockClear();
});
it("does not scroll when `scrollPosition` is null", () => {
service["scrollPosition"] = null;
service.start(virtualElement);
expect(virtualElement.scrollTo).not.toHaveBeenCalled();
});
it("scrolls the virtual element to `scrollPosition`", fakeAsync(() => {
service["scrollPosition"] = 500;
nativeElement.scrollTop = 500;
service.start(virtualElement);
tick();
expect(virtualElement.scrollTo).toHaveBeenCalledWith({ behavior: "instant", top: 500 });
}));
});
describe("scroll listener", () => {
it("unsubscribes from any existing subscription", () => {
service.start(virtualElement);
expect(unsubscribe).toHaveBeenCalled();
});
it("subscribes to `elementScrolled`", fakeAsync(() => {
virtualElement.measureScrollOffset = jest.fn(() => 455);
service.start(virtualElement);
elementScrolled$.next(null); // first subscription is skipped by `skip(1)`
elementScrolled$.next(null);
tick();
expect(virtualElement.measureScrollOffset).toHaveBeenCalledTimes(1);
expect(virtualElement.measureScrollOffset).toHaveBeenCalledWith("top");
expect(service["scrollPosition"]).toBe(455);
}));
});
});
});

View File

@ -0,0 +1,81 @@
import { CdkVirtualScrollableElement } from "@angular/cdk/scrolling";
import { inject, Injectable } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import { filter, skip, Subscription } from "rxjs";
@Injectable({
providedIn: "root",
})
export class VaultPopupScrollPositionService {
private router = inject(Router);
/** Path of the vault screen */
private readonly vaultPath = "/tabs/vault";
/** Current scroll position relative to the top of the viewport. */
private scrollPosition: number | null = null;
/** Subscription associated with the virtual scroll element. */
private scrollSubscription: Subscription | null = null;
constructor() {
this.router.events
.pipe(
takeUntilDestroyed(),
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
)
.subscribe((event) => {
this.resetListenerForNavigation(event);
});
}
/** Scrolls the user to the stored scroll position and starts tracking scroll of the page. */
start(virtualScrollElement: CdkVirtualScrollableElement) {
if (this.hasScrollPosition()) {
// Use `setTimeout` to scroll after rendering is complete
setTimeout(() => {
virtualScrollElement.scrollTo({ top: this.scrollPosition!, behavior: "instant" });
});
}
this.scrollSubscription?.unsubscribe();
// Skip the first scroll event to avoid settings the scroll from the above `scrollTo` call
this.scrollSubscription = virtualScrollElement
?.elementScrolled()
.pipe(skip(1))
.subscribe(() => {
const offset = virtualScrollElement.measureScrollOffset("top");
this.scrollPosition = offset;
});
}
/** Stops the scroll listener from updating the stored location. */
stop(reset?: true) {
this.scrollSubscription?.unsubscribe();
this.scrollSubscription = null;
if (reset) {
this.scrollPosition = null;
}
}
/** Returns true when a scroll position has been stored. */
hasScrollPosition() {
return this.scrollPosition !== null;
}
/** Conditionally resets the scroll listeners based on the ending path of the navigation */
private resetListenerForNavigation(event: NavigationEnd): void {
// The vault page is the target of the scroll listener, return early
if (event.url === this.vaultPath) {
return;
}
// For all other tab pages reset the scroll position
if (event.url.startsWith("/tabs/")) {
this.stop(true);
}
}
}

View File

@ -1,4 +1,4 @@
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
/* eslint-disable no-undef, @typescript-eslint/no-require-imports */
const config = require("../../libs/components/tailwind.config.base");
config.content = [

View File

@ -1,5 +0,0 @@
{
"env": {
"node": true
}
}

View File

@ -1,3 +0,0 @@
{
"extends": "../../../../libs/admin-console/.eslintrc.json"
}

View File

@ -1,6 +0,0 @@
{
"env": {
"browser": true,
"node": true
}
}

View File

@ -1,5 +0,0 @@
{
"rules": {
"no-console": "off"
}
}

View File

@ -1,3 +1,4 @@
/* eslint-disable no-console */
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { homedir } from "os";

View File

@ -1,3 +1,4 @@
/* eslint-disable no-console */
import "module-alias/register";
import { v4 as uuidv4 } from "uuid";

View File

@ -12,9 +12,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService } from "@bitwarden/key-management";
const BroadcasterSubscriptionId = "RegisterComponent";
@ -32,9 +30,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit,
i18nService: I18nService,
keyService: KeyService,
apiService: ApiService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
environmentService: EnvironmentService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
@ -51,9 +47,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit,
i18nService,
keyService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService,
auditService,

View File

@ -22,6 +22,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui";
@ -148,6 +149,16 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
);
}
/**
* Updates the cipher when an attachment is altered.
* Note: This only updates the `attachments` and `revisionDate`
* properties to ensure any in-progress edits are not lost.
*/
patchCipherAttachments(cipher: CipherView) {
this.cipher.attachments = cipher.attachments;
this.cipher.revisionDate = cipher.revisionDate;
}
async importSshKeyFromClipboard(password: string = "") {
const key = await this.platformUtilsService.readFromClipboard();
const parsedKey = await ipc.platform.sshAgent.importKey(key, password);

View File

@ -159,11 +159,6 @@ export class VaultComponent implements OnInit, OnDestroy {
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
await this.vaultFilterComponent.reloadOrganizations();
break;
case "refreshCiphers":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.vaultItemsComponent.refresh();
break;
case "modalShown":
this.showingModal = true;
break;
@ -535,9 +530,19 @@ export class VaultComponent implements OnInit, OnDestroy {
let madeAttachmentChanges = false;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
childComponent.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
childComponent.onUploadedAttachment.subscribe((cipher) => {
madeAttachmentChanges = true;
// Update the edit component cipher with the updated cipher,
// which is needed because the revision date is updated when an attachment is altered
this.addEditComponent.patchCipherAttachments(cipher);
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
childComponent.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
childComponent.onDeletedAttachment.subscribe((cipher) => {
madeAttachmentChanges = true;
// Update the edit component cipher with the updated cipher,
// which is needed because the revision date is updated when an attachment is altered
this.addEditComponent.patchCipherAttachments(cipher);
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.modal.onClosed.subscribe(async () => {

View File

@ -1,4 +1,4 @@
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
/* eslint-disable no-undef, @typescript-eslint/no-require-imports */
const config = require("../../libs/components/tailwind.config.base");
config.content = [

View File

@ -1,22 +0,0 @@
{
"env": {
"browser": true
},
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
"**/app/core/*",
"**/reports/*",
"**/app/shared/*",
"**/organizations/settings/*",
"**/organizations/policies/*",
"@bitwarden/web-vault/*",
"src/**/*",
"bitwarden_license"
]
}
]
}
}

View File

@ -1,3 +0,0 @@
{
"extends": "../../../../../libs/admin-console/.eslintrc.json"
}

View File

@ -7,7 +7,7 @@
<div
class="tw-flex tw-h-32 tw-bg-secondary-100 tw-items-center tw-justify-center tw-pb-2 tw-px-6 lg:tw-pb-4 lg:tw-px-12"
>
<div class="tw-flex tw-items-center tw-justify-center tw-h-28 tw-w-28 lg:tw-w-40">
<div class="tw-flex tw-items-center tw-justify-center tw-size-28 lg:tw-w-40">
<img
#imageEle
[src]="image"
@ -19,7 +19,7 @@
<div class="tw-p-5">
<h3 class="tw-text-main tw-text-lg tw-font-semibold">{{ name }}</h3>
<a
class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-w-full after:tw-h-full after:tw-left-0 after:tw-top-0"
class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-size-full after:tw-left-0 after:tw-top-0"
[href]="linkURL"
rel="noopener noreferrer"
target="_blank"

View File

@ -3,7 +3,6 @@ import { LayoutModule } from "@angular/cdk/layout";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { InfiniteScrollDirective } from "ngx-infinite-scroll";
import { AppComponent } from "./app.component";
import { CoreModule } from "./core";
@ -23,7 +22,6 @@ import { WildcardRoutingModule } from "./wildcard-routing.module";
BrowserAnimationsModule,
FormsModule,
CoreModule,
InfiniteScrollDirective,
DragDropModule,
LayoutModule,
OssRoutingModule,

View File

@ -17,9 +17,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService } from "@bitwarden/key-management";
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
@ -45,9 +43,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn
i18nService: I18nService,
keyService: KeyService,
apiService: ApiService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
private policyService: PolicyService,
environmentService: EnvironmentService,
logService: LogService,
@ -64,9 +60,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn
i18nService,
keyService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService,
auditService,

View File

@ -28,7 +28,7 @@
'!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600':
customColorSelected,
}"
class="tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
class="tw-relative tw-flex tw-size-24 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600"
[style.background-color]="customColor$ | async"
>
<i
@ -37,7 +37,7 @@
></i>
<input
tabindex="-1"
class="tw-absolute tw-bottom-0 tw-right-0 tw-h-px tw-w-px tw-border-none tw-bg-transparent tw-opacity-0"
class="tw-absolute tw-bottom-0 tw-right-0 tw-size-px tw-border-none tw-bg-transparent tw-opacity-0"
#colorPicker
type="color"
[ngModel]="customColor$ | async"

View File

@ -5,7 +5,7 @@
<button
[bitPopoverTriggerFor]="infoPopover"
type="button"
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-flex tw-items-center tw-h-4 tw-w-4"
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-flex tw-items-center tw-size-4"
[position]="'right-start'"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
@ -15,7 +15,7 @@
</bit-popover>
<i
*ngIf="asyncActionLoading"
class="bwi bwi-spinner bwi-spin tw-flex tw-items-center tw-h-4 tw-w-4"
class="bwi bwi-spinner bwi-spin tw-flex tw-items-center tw-size-4"
aria-hidden="true"
></i>
</div>

View File

@ -19,7 +19,7 @@
<div class="tw-mb-1 tw-items-center" *ngIf="annualPlan !== null">
<label class="tw- tw-block tw-text-main" for="annual">
<input
class="tw-h-4 tw-w-4 tw-align-middle"
class="tw-size-4 tw-align-middle"
id="annual"
name="cadence"
type="radio"
@ -34,7 +34,7 @@
<div class="tw-mb-1 tw-items-center" *ngIf="monthlyPlan !== null">
<label class="tw- tw-block tw-text-main" for="monthly">
<input
class="tw-h-4 tw-w-4 tw-align-middle"
class="tw-size-4 tw-align-middle"
id="monthly"
name="cadence"
type="radio"

View File

@ -23,7 +23,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/platform/sync";
import { DialogService, ToastService } from "@bitwarden/components";
import { FreeTrial } from "../../../core/types/free-trial";
import { TrialFlowService } from "../../services/trial-flow.service";
import {
AddCreditDialogResult,
@ -33,6 +32,7 @@ import {
AdjustPaymentDialogComponent,
AdjustPaymentDialogResultType,
} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component";
import { FreeTrial } from "../../types/free-trial";
@Component({
templateUrl: "./organization-payment-method.component.html",

View File

@ -16,11 +16,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
import { FreeTrial } from "../../core/types/free-trial";
import {
ChangePlanDialogResultType,
openChangePlanDialog,
} from "../organizations/change-plan-dialog.component";
import { FreeTrial } from "../types/free-trial";
@Injectable({ providedIn: "root" })
export class TrialFlowService {

View File

@ -24,8 +24,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/platform/sync";
import { DialogService, ToastService } from "@bitwarden/components";
import { FreeTrial } from "../../core/types/free-trial";
import { TrialFlowService } from "../services/trial-flow.service";
import { FreeTrial } from "../types/free-trial";
import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component";
import {

View File

@ -25,13 +25,13 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { ToastService } from "@bitwarden/components";
import { AcceptOrganizationInviteService } from "../../../auth/organization-invite/accept-organization.service";
import {
OrganizationCreatedEvent,
SubscriptionProduct,
TrialOrganizationType,
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
import { RouterService } from "../../../core/router.service";
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
export type InitiationPath =

Some files were not shown because too many files have changed in this diff Show More