1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-18804] generator nudges (#14705)

* added generator spotlight to credential generator component

* moved generator spotlight to browser component and add as slot in libs. update copy for send

* added an aria label for the generator nudge body content

* new copy and styles for browser send will be behind feature flag

* update featureflag call to observable in send-v2

* changed how nudge text is made in credential generator

* added new observable to vault nudges to return specific boolean. Update naming of vault types. update observable calls in credential-generator and send-v2

* update send-v2 and credential generator to use new renamed nudges

* update to create nudge generator spotlight component. using this inside the credential generator for nudge spotlight

* fix imports for Nudge related code

* add libs/angular to storybook

---------

Co-authored-by: Nick Krantz <nick@livefront.com>
This commit is contained in:
Jason Ng
2025-05-21 13:46:02 -04:00
committed by GitHub
parent cf7da2ebdc
commit fd10a26df9
20 changed files with 139 additions and 30 deletions

View File

@@ -22,6 +22,7 @@ const config: StorybookConfig = {
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
"../libs/tools/card/src/**/*.mdx",
"../libs/tools/card/src/**/*.stories.@(js|jsx|ts|tsx)",
"../libs/angular/src/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
getAbsolutePath("@storybook/addon-links"),

View File

@@ -3615,6 +3615,14 @@
"message": "Use Send to securely share encrypted information with anyone.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendsTitleNoItems": {
"message": "Send sensitive information safely",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendsBodyNoItems": {
"message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"inputRequired": {
"message": "Input is required."
},
@@ -5350,6 +5358,23 @@
"description": "Two part message",
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
},
"generatorNudgeTitle": {
"message": "Quickly create passwords"
},
"generatorNudgeBodyOne": {
"message": "Easily create strong and unique passwords by clicking on",
"description": "Two part message",
"example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure."
},
"generatorNudgeBodyTwo": {
"message": "to help you keep your logins secure.",
"description": "Two part message",
"example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure."
},
"generatorNudgeBodyAria": {
"message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.",
"description": "Aria label for the body content of the generator nudge"
},
"noPermissionsViewPage": {
"message": "You do not have permissions to view this page. Try logging in with a different account."
}

View File

@@ -15,6 +15,7 @@ import { filter, firstValueFrom, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import {
@@ -56,7 +57,6 @@ import {
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { SpotlightComponent } from "@bitwarden/vault";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
import { BrowserApi } from "../../../platform/browser/browser-api";

View File

@@ -20,15 +20,29 @@
*ngIf="listState === sendState.Empty"
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
>
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
<tools-new-send-dropdown
[hideIcon]="true"
*ngIf="!sendsDisabled"
slot="button"
></tools-new-send-dropdown>
</bit-no-items>
<ng-container *ngIf="!(showSendSpotlight$ | async)">
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
<tools-new-send-dropdown
[hideIcon]="true"
*ngIf="!sendsDisabled"
slot="button"
></tools-new-send-dropdown>
</bit-no-items>
</ng-container>
<ng-container *ngIf="showSendSpotlight$ | async">
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
<ng-container slot="title">{{ "sendsTitleNoItems" | i18n }}</ng-container>
<ng-container slot="description">{{ "sendsBodyNoItems" | i18n }}</ng-container>
<tools-new-send-dropdown
[hideIcon]="true"
*ngIf="!sendsDisabled"
slot="button"
[buttonType]="'secondary'"
></tools-new-send-dropdown>
</bit-no-items>
</ng-container>
</div>
<ng-container *ngIf="listState !== sendState.Empty">

View File

@@ -6,6 +6,7 @@ import { MockProxy, mock } from "jest-mock-extended";
import { of, BehaviorSubject } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService } from "@bitwarden/angular/vault";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -121,6 +122,7 @@ describe("SendV2Component", () => {
{ provide: SendListFiltersService, useValue: sendListFiltersService },
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
{ provide: PolicyService, useValue: policyService },
{ provide: NudgesService, useValue: mock<NudgesService>() },
],
}).compileComponents();

View File

@@ -1,10 +1,10 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Component, OnDestroy } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { RouterLink } from "@angular/router";
import { combineLatest, switchMap } from "rxjs";
import { combineLatest, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -12,13 +12,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components";
import {
NoSendsIcon,
NewSendDropdownComponent,
SendListItemsContainerComponent,
NoSendsIcon,
SendItemsService,
SendSearchComponent,
SendListFiltersComponent,
SendListFiltersService,
SendListItemsContainerComponent,
SendSearchComponent,
} from "@bitwarden/send-ui";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
@@ -46,14 +46,13 @@ export enum SendState {
JslibModule,
CommonModule,
ButtonModule,
RouterLink,
NewSendDropdownComponent,
SendListItemsContainerComponent,
SendListFiltersComponent,
SendSearchComponent,
],
})
export class SendV2Component implements OnInit, OnDestroy {
export class SendV2Component implements OnDestroy {
sendType = SendType;
sendState = SendState;
@@ -63,6 +62,12 @@ export class SendV2Component implements OnInit, OnDestroy {
protected title: string = "allSends";
protected noItemIcon = NoSendsIcon;
protected noResultsIcon = Icons.NoResults;
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
protected showSendSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
switchMap((userId) =>
this.nudgesService.showNudgeSpotlight$(NudgeType.SendNudgeStatus, userId),
),
);
protected sendsDisabled = false;
@@ -71,6 +76,7 @@ export class SendV2Component implements OnInit, OnDestroy {
protected sendListFiltersService: SendListFiltersService,
private policyService: PolicyService,
private accountService: AccountService,
private nudgesService: NudgesService,
) {
combineLatest([
this.sendItemsService.emptyList$,
@@ -111,7 +117,5 @@ export class SendV2Component implements OnInit, OnDestroy {
});
}
ngOnInit(): void {}
ngOnDestroy(): void {}
}

View File

@@ -17,10 +17,10 @@ import {
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -31,7 +31,7 @@ import {
NoItemsModule,
TypographyModule,
} from "@bitwarden/components";
import { DecryptionFailureDialogComponent, SpotlightComponent, VaultIcons } from "@bitwarden/vault";
import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
import { BrowserApi } from "../../../../platform/browser/browser-api";
@@ -154,7 +154,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private introCarouselService: IntroCarouselService,
private nudgesService: NudgesService,
private router: Router,
private i18nService: I18nService,
) {
combineLatest([
this.vaultPopupItemsService.emptyVault$,

View File

@@ -40,6 +40,8 @@ export enum NudgeType {
NewIdentityItemStatus = "new-identity-item-status",
NewNoteItemStatus = "new-note-item-status",
NewSshItemStatus = "new-ssh-item-status",
GeneratorNudgeStatus = "generator-nudge-status",
SendNudgeStatus = "send-nudge-status",
}
export const NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<

View File

@@ -11,6 +11,8 @@
</bit-toggle>
</bit-toggle-group>
<nudge-generator-spotlight></nudge-generator-spotlight>
<bit-card class="tw-flex tw-justify-between tw-mb-4">
<div class="tw-grow tw-flex tw-items-center">
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>

View File

@@ -22,6 +22,7 @@ import { CatchallSettingsComponent } from "./catchall-settings.component";
import { CredentialGeneratorComponent } from "./credential-generator.component";
import { ForwarderSettingsComponent } from "./forwarder-settings.component";
import { GeneratorServicesModule } from "./generator-services.module";
import { NudgeGeneratorSpotlightComponent } from "./nudge-generator-spotlight.component";
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
import { PasswordGeneratorComponent } from "./password-generator.component";
import { PasswordSettingsComponent } from "./password-settings.component";
@@ -48,6 +49,7 @@ import { UsernameSettingsComponent } from "./username-settings.component";
SelectModule,
ToggleGroupModule,
TypographyModule,
NudgeGeneratorSpotlightComponent,
],
declarations: [
CatchallSettingsComponent,

View File

@@ -0,0 +1,15 @@
<div class="tw-mb-4" *ngIf="showGeneratorSpotlight$ | async">
<bit-spotlight
[title]="'generatorNudgeTitle' | i18n"
(onDismiss)="dismissGeneratorSpotlight(NudgeType.GeneratorNudgeStatus)"
>
<p
class="tw-text-main"
bitTypography="body2"
[attr.aria-label]="'generatorNudgeIconAria' | i18n"
>
{{ "generatorNudgeBodyOne" | i18n }} <i class="bwi bwi-generate"></i>
{{ "generatorNudgeBodyTwo" | i18n }}
</p>
</bit-spotlight>
</div>

View File

@@ -0,0 +1,38 @@
import { AsyncPipe, CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { firstValueFrom, Observable, switchMap } from "rxjs";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { TypographyModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
@Component({
standalone: true,
selector: "nudge-generator-spotlight",
templateUrl: "nudge-generator-spotlight.component.html",
imports: [I18nPipe, SpotlightComponent, AsyncPipe, CommonModule, TypographyModule],
})
export class NudgeGeneratorSpotlightComponent {
protected readonly NudgeType = NudgeType;
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
protected showGeneratorSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
switchMap((userId) =>
this.nudgesService.showNudgeSpotlight$(NudgeType.GeneratorNudgeStatus, userId),
),
);
constructor(
private nudgesService: NudgesService,
private accountService: AccountService,
) {}
async dismissGeneratorSpotlight(type: NudgeType) {
const activeUserId = await firstValueFrom(this.activeUserId$);
await this.nudgesService.dismissNudge(type, activeUserId as UserId);
}
}

View File

@@ -11,7 +11,8 @@
"@bitwarden/generator-history": ["../../../tools/generator/extensions/history/src"],
"@bitwarden/key-management": ["../../../key-management/src"],
"@bitwarden/platform": ["../../../platform/src"],
"@bitwarden/ui-common": ["../../../ui/common/src"]
"@bitwarden/ui-common": ["../../../ui/common/src"],
"@bitwarden/vault": ["../../../vault/src"]
}
},
"include": ["src"],

View File

@@ -1,4 +1,10 @@
<button bitButton size="small" [bitMenuTriggerFor]="itemOptions" buttonType="primary" type="button">
<button
bitButton
size="small"
[bitMenuTriggerFor]="itemOptions"
[buttonType]="buttonType"
type="button"
>
<i *ngIf="!hideIcon" class="bwi bwi-plus" aria-hidden="true"></i>
{{ (hideIcon ? "createSend" : "new") | i18n }}
</button>

View File

@@ -7,7 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components";
import { BadgeModule, ButtonModule, ButtonType, MenuModule } from "@bitwarden/components";
@Component({
selector: "tools-new-send-dropdown",
@@ -17,6 +17,7 @@ import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components";
})
export class NewSendDropdownComponent implements OnInit {
@Input() hideIcon: boolean = false;
@Input() buttonType: ButtonType = "primary";
sendType = SendType;

View File

@@ -3,14 +3,13 @@ import { Component, Input, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/sdk-internal";
import { SpotlightComponent } from "../../../components/spotlight/spotlight.component";
@Component({
selector: "vault-new-item-nudge",
templateUrl: "./new-item-nudge.component.html",

View File

@@ -27,5 +27,3 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service";
export * from "./abstractions/change-login-password.service";
export * from "./services/default-change-login-password.service";
export { SpotlightComponent } from "./components/spotlight/spotlight.component";