diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 4b25c588090..096fff8db34 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -168,10 +168,6 @@ jobs: run: npm run dist:chrome:beta working-directory: browser-source/apps/browser - - name: Gulp - run: gulp ci - working-directory: browser-source/apps/browser - - name: Upload Opera artifact uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: @@ -235,14 +231,6 @@ jobs: path: browser-source.zip if-no-files-found: error - - name: Upload coverage artifact - if: false - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 - with: - name: coverage-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/coverage/coverage-${{ env._BUILD_NUMBER }}.zip - if-no-files-found: error - build-safari: name: Build Safari runs-on: macos-13 diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index ac771e7157f..53a0e886267 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -14,7 +14,6 @@ const betaBuild = process.env.BETA_BUILD === "1"; const paths = { build: "./build/", dist: "./dist/", - coverage: "./coverage/", node_modules: "./node_modules/", popupDir: "./src/popup/", cssDir: "./src/popup/css/", @@ -276,17 +275,6 @@ function stdOutProc(proc) { proc.stderr.on("data", (data) => console.error(data.toString())); } -async function ciCoverage(cb) { - const { default: zip } = await import("gulp-zip"); - const { default: filter } = await import("gulp-filter"); - - return gulp - .src(paths.coverage + "**/*") - .pipe(filter(["**", "!coverage/coverage*.zip"])) - .pipe(zip(`coverage${buildString()}.zip`)) - .pipe(gulp.dest(paths.coverage)); -} - function applyBetaLabels(manifest) { manifest.name = "Bitwarden Password Manager BETA"; manifest.short_name = "Bitwarden BETA"; @@ -320,5 +308,3 @@ exports["dist:safari:mas"] = distSafariMas; exports["dist:safari:masdev"] = distSafariMasDev; exports["dist:safari:dmg"] = distSafariDmg; exports.dist = gulp.parallel(distFirefox, distChrome, distOpera, distEdge); -exports["ci:coverage"] = ciCoverage; -exports.ci = ciCoverage; diff --git a/apps/browser/package.json b/apps/browser/package.json index 15caff9eff2..6c41f6267cc 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -26,7 +26,6 @@ "dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev", "dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg", "test": "jest", - "test:coverage": "jest --coverage --coverageDirectory=coverage", "test:watch": "jest --watch", "test:watch:all": "jest --watchAll" } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index ec0fac137df..7b30e5aa334 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -381,7 +381,7 @@ }, "generator": { "message": "Generator", - "description": "Short for 'Password Generator'." + "description": "Short for 'credential generator'." }, "passGenInfo": { "message": "Automatically generate strong, unique passwords for your logins." @@ -3662,6 +3662,9 @@ "noMatchingLoginsForSite": { "message": "No matching logins for this site" }, + "searchSavePasskeyNewLogin": { + "message": "Search or save passkey as new login" + }, "confirm": { "message": "Confirm" }, diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.html b/apps/browser/src/autofill/popup/fido2/fido2.component.html index 00cd55d31b5..dc9f3ff83ff 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.html @@ -50,7 +50,7 @@ {{ "noMatchingLoginsForSite" | i18n }} - Search or save passkey as new login + {{ "searchSavePasskeyNewLogin" | i18n }} + + + + + + + +
{{ "options" | i18n }}
+
+
+ +
+ + {{ "type" | i18n }} + + {{ + credentialTypeHint$ | async + }} + +
+ + + +
+
+
diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts new file mode 100644 index 00000000000..359c7505c54 --- /dev/null +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -0,0 +1,293 @@ +import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { + BehaviorSubject, + concat, + distinctUntilChanged, + filter, + map, + of, + ReplaySubject, + Subject, + switchMap, + takeUntil, + withLatestFrom, +} from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { Option } from "@bitwarden/components/src/select/option"; +import { + CredentialAlgorithm, + CredentialCategory, + CredentialGeneratorInfo, + CredentialGeneratorService, + GeneratedCredential, + Generators, + isEmailAlgorithm, + isPasswordAlgorithm, + isUsernameAlgorithm, + PasswordAlgorithm, +} from "@bitwarden/generator-core"; + +/** root category that drills into username and email categories */ +const IDENTIFIER = "identifier"; +/** options available for the top-level navigation */ +type RootNavValue = PasswordAlgorithm | typeof IDENTIFIER; + +@Component({ + selector: "tools-credential-generator", + templateUrl: "credential-generator.component.html", +}) +export class CredentialGeneratorComponent implements OnInit, OnDestroy { + constructor( + private generatorService: CredentialGeneratorService, + private i18nService: I18nService, + private accountService: AccountService, + private zone: NgZone, + private formBuilder: FormBuilder, + ) {} + + /** Binds the component to a specific user's settings. When this input is not provided, + * the form binds to the active user + */ + @Input() + userId: UserId | null; + + /** Emits credentials created from a generation request. */ + @Output() + readonly onGenerated = new EventEmitter(); + + protected root$ = new BehaviorSubject<{ nav: RootNavValue }>({ + nav: null, + }); + + protected onRootChanged(nav: RootNavValue) { + // prevent subscription cycle + if (this.root$.value.nav !== nav) { + this.zone.run(() => { + this.root$.next({ nav }); + }); + } + } + + protected username = this.formBuilder.group({ + nav: [null as CredentialAlgorithm], + }); + + async ngOnInit() { + if (this.userId) { + this.userId$.next(this.userId); + } else { + this.accountService.activeAccount$ + .pipe( + map((acct) => acct.id), + distinctUntilChanged(), + takeUntil(this.destroyed), + ) + .subscribe(this.userId$); + } + + this.generatorService + .algorithms$(["email", "username"], { userId$: this.userId$ }) + .pipe( + map((algorithms) => this.toOptions(algorithms)), + takeUntil(this.destroyed), + ) + .subscribe(this.usernameOptions$); + + this.generatorService + .algorithms$("password", { userId$: this.userId$ }) + .pipe( + map((algorithms) => { + const options = this.toOptions(algorithms) as Option[]; + options.push({ value: IDENTIFIER, label: this.i18nService.t("username") }); + return options; + }), + takeUntil(this.destroyed), + ) + .subscribe(this.rootOptions$); + + this.algorithm$ + .pipe( + map((a) => a?.descriptionKey && this.i18nService.t(a?.descriptionKey)), + takeUntil(this.destroyed), + ) + .subscribe((hint) => { + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.credentialTypeHint$.next(hint); + }); + }); + + this.algorithm$ + .pipe( + map((a) => a.category), + distinctUntilChanged(), + takeUntil(this.destroyed), + ) + .subscribe((category) => { + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.category$.next(category); + }); + }); + + // wire up the generator + this.algorithm$ + .pipe( + switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), + takeUntil(this.destroyed), + ) + .subscribe((generated) => { + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.onGenerated.next(generated); + this.value$.next(generated.credential); + }); + }); + + // assume the last-visible generator algorithm is the user's preferred one + const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ }); + this.root$ + .pipe( + filter(({ nav }) => !!nav), + switchMap((root) => { + if (root.nav === IDENTIFIER) { + return concat(of(this.username.value), this.username.valueChanges); + } else { + return of(root as { nav: PasswordAlgorithm }); + } + }), + filter(({ nav }) => !!nav), + withLatestFrom(preferences), + takeUntil(this.destroyed), + ) + .subscribe(([{ nav: algorithm }, preference]) => { + function setPreference(category: CredentialCategory) { + const p = preference[category]; + p.algorithm = algorithm; + p.updated = new Date(); + } + + // `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference` + if (isEmailAlgorithm(algorithm)) { + setPreference("email"); + } else if (isUsernameAlgorithm(algorithm)) { + setPreference("username"); + } else if (isPasswordAlgorithm(algorithm)) { + setPreference("password"); + } else { + return; + } + + preferences.next(preference); + }); + + // populate the form with the user's preferences to kick off interactivity + preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username, password }) => { + // the last preference set by the user "wins" + const userNav = email.updated > username.updated ? email : username; + const rootNav: any = userNav.updated > password.updated ? IDENTIFIER : password.algorithm; + const credentialType = rootNav === IDENTIFIER ? userNav.algorithm : password.algorithm; + + // update navigation; break subscription loop + this.onRootChanged(rootNav); + this.username.setValue({ nav: userNav.algorithm }, { emitEvent: false }); + + // load algorithm metadata + const algorithm = this.generatorService.algorithm(credentialType); + + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.algorithm$.next(algorithm); + }); + }); + + // generate on load unless the generator prohibits it + this.algorithm$ + .pipe( + distinctUntilChanged((prev, next) => prev.id === next.id), + filter((a) => !a.onlyOnRequest), + takeUntil(this.destroyed), + ) + .subscribe(() => this.generate$.next()); + } + + private typeToGenerator$(type: CredentialAlgorithm) { + const dependencies = { + on$: this.generate$, + userId$: this.userId$, + }; + + switch (type) { + case "catchall": + return this.generatorService.generate$(Generators.catchall, dependencies); + + case "subaddress": + return this.generatorService.generate$(Generators.subaddress, dependencies); + + case "username": + return this.generatorService.generate$(Generators.username, dependencies); + + case "password": + return this.generatorService.generate$(Generators.password, dependencies); + + case "passphrase": + return this.generatorService.generate$(Generators.passphrase, dependencies); + + default: + throw new Error(`Invalid generator type: "${type}"`); + } + } + + /** Lists the credential types of the username algorithm box. */ + protected usernameOptions$ = new BehaviorSubject[]>([]); + + /** Lists the top-level credential types supported by the component. */ + protected rootOptions$ = new BehaviorSubject[]>([]); + + /** tracks the currently selected credential type */ + protected algorithm$ = new ReplaySubject(1); + + /** Emits hint key for the currently selected credential type */ + protected credentialTypeHint$ = new ReplaySubject(1); + + /** tracks the currently selected credential category */ + protected category$ = new ReplaySubject(1); + + /** Emits the last generated value. */ + protected readonly value$ = new BehaviorSubject(""); + + /** Emits when the userId changes */ + protected readonly userId$ = new BehaviorSubject(null); + + /** Emits when a new credential is requested */ + protected readonly generate$ = new Subject(); + + private toOptions(algorithms: CredentialGeneratorInfo[]) { + const options: Option[] = algorithms.map((algorithm) => ({ + value: algorithm.id, + label: this.i18nService.t(algorithm.nameKey), + })); + + return options; + } + + private readonly destroyed = new Subject(); + ngOnDestroy() { + this.destroyed.complete(); + + // finalize subjects + this.generate$.complete(); + this.value$.complete(); + + // finalize component bindings + this.onGenerated.complete(); + } +} diff --git a/libs/tools/generator/components/src/dependencies.ts b/libs/tools/generator/components/src/generator.module.ts similarity index 62% rename from libs/tools/generator/components/src/dependencies.ts rename to libs/tools/generator/components/src/generator.module.ts index 6f2c13e0579..c7dfc60bab2 100644 --- a/libs/tools/generator/components/src/dependencies.ts +++ b/libs/tools/generator/components/src/generator.module.ts @@ -10,8 +10,8 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { StateProvider } from "@bitwarden/common/platform/state"; import { CardComponent, - CheckboxModule, ColorPasswordModule, + CheckboxModule, FormFieldModule, IconButtonModule, InputModule, @@ -27,16 +27,24 @@ import { Randomizer, } from "@bitwarden/generator-core"; +import { CatchallSettingsComponent } from "./catchall-settings.component"; +import { CredentialGeneratorComponent } from "./credential-generator.component"; +import { PassphraseSettingsComponent } from "./passphrase-settings.component"; +import { PasswordGeneratorComponent } from "./password-generator.component"; +import { PasswordSettingsComponent } from "./password-settings.component"; +import { SubaddressSettingsComponent } from "./subaddress-settings.component"; +import { UsernameGeneratorComponent } from "./username-generator.component"; +import { UsernameSettingsComponent } from "./username-settings.component"; + const RANDOMIZER = new SafeInjectionToken("Randomizer"); /** Shared module containing generator component dependencies */ @NgModule({ - imports: [CardComponent, SectionComponent, SectionHeaderComponent], - exports: [ + imports: [ CardComponent, + ColorPasswordModule, CheckboxModule, CommonModule, - ColorPasswordModule, FormFieldModule, IconButtonModule, InputModule, @@ -60,8 +68,18 @@ const RANDOMIZER = new SafeInjectionToken("Randomizer"); deps: [RANDOMIZER, StateProvider, PolicyService], }), ], - declarations: [], + declarations: [ + CatchallSettingsComponent, + CredentialGeneratorComponent, + SubaddressSettingsComponent, + UsernameSettingsComponent, + PasswordGeneratorComponent, + PasswordSettingsComponent, + PassphraseSettingsComponent, + UsernameGeneratorComponent, + ], + exports: [CredentialGeneratorComponent, PasswordGeneratorComponent, UsernameGeneratorComponent], }) -export class DependenciesModule { +export class GeneratorModule { constructor() {} } diff --git a/libs/tools/generator/components/src/index.ts b/libs/tools/generator/components/src/index.ts index 9367a32546c..213461174f0 100644 --- a/libs/tools/generator/components/src/index.ts +++ b/libs/tools/generator/components/src/index.ts @@ -1,9 +1,3 @@ -export { CatchallSettingsComponent } from "./catchall-settings.component"; export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component"; export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component"; -export { PassphraseSettingsComponent } from "./passphrase-settings.component"; -export { PasswordSettingsComponent } from "./password-settings.component"; -export { PasswordGeneratorComponent } from "./password-generator.component"; -export { SubaddressSettingsComponent } from "./subaddress-settings.component"; -export { UsernameGeneratorComponent } from "./username-generator.component"; -export { UsernameSettingsComponent } from "./username-settings.component"; +export { GeneratorModule } from "./generator.module"; diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index bfb3425bf63..25e028210cc 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -10,7 +10,6 @@ import { PassphraseGenerationOptions, } from "@bitwarden/generator-core"; -import { DependenciesModule } from "./dependencies"; import { completeOnAccountSwitch, toValidators } from "./util"; const Controls = Object.freeze({ @@ -22,10 +21,8 @@ const Controls = Object.freeze({ /** Options group for passphrases */ @Component({ - standalone: true, selector: "tools-passphrase-settings", templateUrl: "passphrase-settings.component.html", - imports: [DependenciesModule], }) export class PassphraseSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index 62bcdfa15da..7ec3a565dd3 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -5,11 +5,8 @@ (selectedChange)="onCredentialTypeChanged($event)" attr.aria-label="{{ 'type' | i18n }}" > - - {{ "password" | i18n }} - - - {{ "passphrase" | i18n }} + + {{ option.label }} @@ -24,6 +21,7 @@ type="button" bitIconButton="bwi-clone" buttonType="main" + showToast [appCopyClick]="value$ | async" > {{ "copyPassword" | i18n }} @@ -32,13 +30,13 @@ diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index b6d8fbf60da..bf33c7cfca9 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -1,29 +1,39 @@ import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, distinctUntilChanged, map, Subject, switchMap, takeUntil } from "rxjs"; +import { + BehaviorSubject, + distinctUntilChanged, + filter, + map, + ReplaySubject, + Subject, + switchMap, + takeUntil, + withLatestFrom, +} from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { Option } from "@bitwarden/components/src/select/option"; import { CredentialGeneratorService, Generators, PasswordAlgorithm, GeneratedCredential, + CredentialGeneratorInfo, + CredentialAlgorithm, + isPasswordAlgorithm, } from "@bitwarden/generator-core"; -import { DependenciesModule } from "./dependencies"; -import { PassphraseSettingsComponent } from "./passphrase-settings.component"; -import { PasswordSettingsComponent } from "./password-settings.component"; - /** Options group for passwords */ @Component({ - standalone: true, selector: "tools-password-generator", templateUrl: "password-generator.component.html", - imports: [DependenciesModule, PasswordSettingsComponent, PassphraseSettingsComponent], }) export class PasswordGeneratorComponent implements OnInit, OnDestroy { constructor( private generatorService: CredentialGeneratorService, + private i18nService: I18nService, private accountService: AccountService, private zone: NgZone, ) {} @@ -36,7 +46,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { userId: UserId | null; /** tracks the currently selected credential type */ - protected credentialType$ = new BehaviorSubject("password"); + protected credentialType$ = new BehaviorSubject(null); /** Emits the last generated value. */ protected readonly value$ = new BehaviorSubject(""); @@ -51,9 +61,11 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { * @param type the new credential type */ protected onCredentialTypeChanged(type: PasswordAlgorithm) { + // break subscription cycle if (this.credentialType$.value !== type) { - this.credentialType$.next(type); - this.generate$.next(); + this.zone.run(() => { + this.credentialType$.next(type); + }); } } @@ -74,9 +86,18 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { .subscribe(this.userId$); } - this.credentialType$ + this.generatorService + .algorithms$("password", { userId$: this.userId$ }) .pipe( - switchMap((type) => this.typeToGenerator$(type)), + map((algorithms) => this.toOptions(algorithms)), + takeUntil(this.destroyed), + ) + .subscribe(this.passwordOptions$); + + // wire up the generator + this.algorithm$ + .pipe( + switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), takeUntil(this.destroyed), ) .subscribe((generated) => { @@ -87,9 +108,52 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { this.value$.next(generated.credential); }); }); + + // assume the last-visible generator algorithm is the user's preferred one + const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ }); + this.credentialType$ + .pipe( + filter((type) => !!type), + withLatestFrom(preferences), + takeUntil(this.destroyed), + ) + .subscribe(([algorithm, preference]) => { + if (isPasswordAlgorithm(algorithm)) { + preference.password.algorithm = algorithm; + preference.password.updated = new Date(); + } else { + return; + } + + preferences.next(preference); + }); + + // populate the form with the user's preferences to kick off interactivity + preferences.pipe(takeUntil(this.destroyed)).subscribe(({ password }) => { + // update navigation + this.onCredentialTypeChanged(password.algorithm); + + // load algorithm metadata + const algorithm = this.generatorService.algorithm(password.algorithm); + + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.algorithm$.next(algorithm); + }); + }); + + // generate on load unless the generator prohibits it + this.algorithm$ + .pipe( + distinctUntilChanged((prev, next) => prev.id === next.id), + filter((a) => !a.onlyOnRequest), + takeUntil(this.destroyed), + ) + .subscribe(() => this.generate$.next()); } - private typeToGenerator$(type: PasswordAlgorithm) { + private typeToGenerator$(type: CredentialAlgorithm) { const dependencies = { on$: this.generate$, userId$: this.userId$, @@ -106,6 +170,21 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { } } + /** Lists the credential types supported by the component. */ + protected passwordOptions$ = new BehaviorSubject[]>([]); + + /** tracks the currently selected credential type */ + protected algorithm$ = new ReplaySubject(1); + + private toOptions(algorithms: CredentialGeneratorInfo[]) { + const options: Option[] = algorithms.map((algorithm) => ({ + value: algorithm.id, + label: this.i18nService.t(algorithm.nameKey), + })); + + return options; + } + private readonly destroyed = new Subject(); ngOnDestroy(): void { // tear down subscriptions diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index d9fd6cd99c9..9466c81a0f4 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -10,7 +10,6 @@ import { PasswordGenerationOptions, } from "@bitwarden/generator-core"; -import { DependenciesModule } from "./dependencies"; import { completeOnAccountSwitch, toValidators } from "./util"; const Controls = Object.freeze({ @@ -26,10 +25,8 @@ const Controls = Object.freeze({ /** Options group for passwords */ @Component({ - standalone: true, selector: "tools-password-settings", templateUrl: "password-settings.component.html", - imports: [DependenciesModule], }) export class PasswordSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component diff --git a/libs/tools/generator/components/src/subaddress-settings.component.ts b/libs/tools/generator/components/src/subaddress-settings.component.ts index ed55cb51ba0..30db8dc657d 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.ts +++ b/libs/tools/generator/components/src/subaddress-settings.component.ts @@ -10,15 +10,12 @@ import { SubaddressGenerationOptions, } from "@bitwarden/generator-core"; -import { DependenciesModule } from "./dependencies"; import { completeOnAccountSwitch } from "./util"; /** Options group for plus-addressed emails */ @Component({ - standalone: true, selector: "tools-subaddress-settings", templateUrl: "subaddress-settings.component.html", - imports: [DependenciesModule], }) export class SubaddressSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index 413de931452..a44637d78e5 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -10,6 +10,7 @@ type="button" bitIconButton="bwi-clone" buttonType="main" + showToast [appCopyClick]="value$ | async" > {{ "copyPassword" | i18n }} diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index e5327cc66eb..767c73c398a 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -26,23 +26,10 @@ import { isUsernameAlgorithm, } from "@bitwarden/generator-core"; -import { CatchallSettingsComponent } from "./catchall-settings.component"; -import { DependenciesModule } from "./dependencies"; -import { SubaddressSettingsComponent } from "./subaddress-settings.component"; -import { UsernameSettingsComponent } from "./username-settings.component"; -import { completeOnAccountSwitch } from "./util"; - /** Component that generates usernames and emails */ @Component({ - standalone: true, selector: "tools-username-generator", templateUrl: "username-generator.component.html", - imports: [ - DependenciesModule, - CatchallSettingsComponent, - SubaddressSettingsComponent, - UsernameSettingsComponent, - ], }) export class UsernameGeneratorComponent implements OnInit, OnDestroy { /** Instantiates the username generator @@ -72,14 +59,20 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { /** Tracks the selected generation algorithm */ protected credential = this.formBuilder.group({ - type: ["username" as CredentialAlgorithm], + type: [null as CredentialAlgorithm], }); async ngOnInit() { if (this.userId) { this.userId$.next(this.userId); } else { - this.singleUserId$().pipe(takeUntil(this.destroyed)).subscribe(this.userId$); + this.accountService.activeAccount$ + .pipe( + map((acct) => acct.id), + distinctUntilChanged(), + takeUntil(this.destroyed), + ) + .subscribe(this.userId$); } this.generatorService @@ -121,7 +114,11 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // assume the last-visible generator algorithm is the user's preferred one const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ }); this.credential.valueChanges - .pipe(withLatestFrom(preferences), takeUntil(this.destroyed)) + .pipe( + filter(({ type }) => !!type), + withLatestFrom(preferences), + takeUntil(this.destroyed), + ) .subscribe(([{ type }, preference]) => { if (isEmailAlgorithm(type)) { preference.email.algorithm = type; @@ -202,19 +199,6 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { /** Emits when a new credential is requested */ protected readonly generate$ = new Subject(); - private singleUserId$() { - // FIXME: this branch should probably scan for the user and make sure - // the account is unlocked - if (this.userId) { - return new BehaviorSubject(this.userId as UserId).asObservable(); - } - - return this.accountService.activeAccount$.pipe( - completeOnAccountSwitch(), - takeUntil(this.destroyed), - ); - } - private toOptions(algorithms: CredentialGeneratorInfo[]) { const options: Option[] = algorithms.map((algorithm) => ({ value: algorithm.id, diff --git a/libs/tools/generator/components/src/username-settings.component.ts b/libs/tools/generator/components/src/username-settings.component.ts index 978bd05ca79..8237b8674cd 100644 --- a/libs/tools/generator/components/src/username-settings.component.ts +++ b/libs/tools/generator/components/src/username-settings.component.ts @@ -10,15 +10,12 @@ import { Generators, } from "@bitwarden/generator-core"; -import { DependenciesModule } from "./dependencies"; import { completeOnAccountSwitch } from "./util"; /** Options group for usernames */ @Component({ - standalone: true, selector: "tools-username-settings", templateUrl: "username-settings.component.html", - imports: [DependenciesModule], }) export class UsernameSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 28c18b3665f..f00aacf9631 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -1,8 +1,8 @@ +import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; /** diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 96d4289028d..13f233f53d1 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -9,16 +9,18 @@ import { } from "@storybook/angular"; import { BehaviorSubject } from "rxjs"; +import { CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ClientType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { AsyncActionsModule, ButtonModule, ToastService } from "@bitwarden/components"; @@ -175,6 +177,12 @@ export default { collect: () => Promise.resolve(), }, }, + { + provide: PlatformUtilsService, + useValue: { + getClientType: () => ClientType.Browser, + }, + }, ], }), componentWrapperDecorator( diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts index e4dba11525b..f1e5af2868a 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts @@ -7,6 +7,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; @@ -23,10 +24,12 @@ describe("AutofillOptionsComponent", () => { let liveAnnouncer: MockProxy; let domainSettingsService: MockProxy; let autofillSettingsService: MockProxy; + let platformUtilsService: MockProxy; beforeEach(async () => { cipherFormContainer = mock(); liveAnnouncer = mock(); + platformUtilsService = mock(); domainSettingsService = mock(); domainSettingsService.defaultUriMatchStrategy$ = new BehaviorSubject(null); @@ -45,6 +48,7 @@ describe("AutofillOptionsComponent", () => { { provide: LiveAnnouncer, useValue: liveAnnouncer }, { provide: DomainSettingsService, useValue: domainSettingsService }, { provide: AutofillSettingsServiceAbstraction, useValue: autofillSettingsService }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, ], }).compileComponents(); diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index eb5767b534f..40f7eeecf07 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -3,13 +3,15 @@ import { AsyncPipe, NgForOf, NgIf } from "@angular/common"; import { Component, OnInit, QueryList, ViewChildren } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; -import { Subject, switchMap, take } from "rxjs"; +import { filter, Subject, switchMap, take } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ClientType } from "@bitwarden/common/enums"; import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { @@ -69,7 +71,10 @@ export class AutofillOptionsComponent implements OnInit { return this.autofillOptionsForm.controls.uris.controls; } - protected defaultMatchDetection$ = this.domainSettingsService.defaultUriMatchStrategy$; + protected defaultMatchDetection$ = this.domainSettingsService.defaultUriMatchStrategy$.pipe( + // The default match detection should only be shown when used on the browser + filter(() => this.platformUtilsService.getClientType() == ClientType.Browser), + ); protected autofillOnPageLoadEnabled$ = this.autofillSettingsService.autofillOnPageLoad$; protected autofillOptions: { label: string; value: boolean | null }[] = [ @@ -90,6 +95,7 @@ export class AutofillOptionsComponent implements OnInit { private liveAnnouncer: LiveAnnouncer, private domainSettingsService: DomainSettingsService, private autofillSettingsService: AutofillSettingsServiceAbstraction, + private platformUtilsService: PlatformUtilsService, ) { this.cipherFormContainer.registerChildForm("autoFillOptions", this.autofillOptionsForm); diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts index cf298b19ac7..fdb306ff761 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts @@ -51,6 +51,13 @@ describe("UriOptionComponent", () => { expect(component).toBeTruthy(); }); + it("should not update the default uri match strategy label when it is null", () => { + component.defaultMatchDetection = null; + fixture.detectChanges(); + + expect(component["uriMatchOptions"][0].label).toBe("default"); + }); + it("should update the default uri match strategy label", () => { component.defaultMatchDetection = UriMatchStrategy.Exact; fixture.detectChanges(); diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts index a7741ae1bc6..82870befa12 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts @@ -83,6 +83,11 @@ export class UriOptionComponent implements ControlValueAccessor { */ @Input({ required: true }) set defaultMatchDetection(value: UriMatchStrategySetting) { + // The default selection has a value of `null` avoid showing "Default (Default)" + if (!value) { + return; + } + this.uriMatchOptions[0].label = this.i18nService.t( "defaultLabel", this.uriMatchOptions.find((o) => o.value === value)?.label, diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts index 48b1250eb1b..8adda7d1a06 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts @@ -3,10 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { - PasswordGeneratorComponent, - UsernameGeneratorComponent, -} from "@bitwarden/generator-components"; +import { GeneratorModule } from "@bitwarden/generator-components"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; @Component({ @@ -37,7 +34,7 @@ describe("CipherFormGeneratorComponent", () => { providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], }) .overrideComponent(CipherFormGeneratorComponent, { - remove: { imports: [PasswordGeneratorComponent, UsernameGeneratorComponent] }, + remove: { imports: [GeneratorModule] }, add: { imports: [MockPasswordGeneratorComponent, MockUsernameGeneratorComponent] }, }) .compileComponents(); diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index ee06e601ad4..db6e9ae106b 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -1,11 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { SectionComponent } from "@bitwarden/components"; -import { - PasswordGeneratorComponent, - UsernameGeneratorComponent, -} from "@bitwarden/generator-components"; +import { GeneratorModule } from "@bitwarden/generator-components"; import { GeneratedCredential } from "@bitwarden/generator-core"; /** @@ -16,7 +12,7 @@ import { GeneratedCredential } from "@bitwarden/generator-core"; selector: "vault-cipher-form-generator", templateUrl: "./cipher-form-generator.component.html", standalone: true, - imports: [CommonModule, SectionComponent, PasswordGeneratorComponent, UsernameGeneratorComponent], + imports: [CommonModule, GeneratorModule], }) export class CipherFormGeneratorComponent { /** diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index d7678aa596a..fe1710ccade 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -3,11 +3,11 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testin import { ReactiveFormsModule } from "@angular/forms"; import { mock, MockProxy } from "jest-mock-extended"; +import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CipherFormConfig } from "../../abstractions/cipher-form-config.service"; import { CipherFormContainer } from "../../cipher-form-container"; diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index b0716218b59..06ce363a270 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -4,12 +4,12 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; import { concatMap, map } from "rxjs"; +import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CardComponent, FormFieldModule, diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 977cee8156d..6b607e3048b 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -1,12 +1,12 @@ import { inject, Injectable } from "@angular/core"; import { combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index c73ad149400..324b2358a8c 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -2,15 +2,14 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; +import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; import { CollectionId } from "@bitwarden/common/types/guid"; -import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CalloutModule, SearchModule } from "@bitwarden/components"; diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index 30e8ad1ecdb..48c129bd3b5 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -1,10 +1,10 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; +import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CardComponent, diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index dbbf4e7bcf2..423bd57dfb4 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -36,15 +36,6 @@ aria-readonly="true" data-testid="login-password" /> - +