1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PM-25879][PM-25881] - [Defect] Premium badges missing reusable component (#16461)

* clean up premium badge component

* add provider to desktop settings

* rename prop.

* add provider to send component

* fix storybook

* fix test

* move dependency to new send dropdown component

* Revert "move dependency to new send dropdown component"

This reverts commit f134526279.

* remove hasPremium
This commit is contained in:
Jordan Aasen
2025-09-23 09:32:45 -07:00
committed by GitHub
parent 7313a5f2a3
commit 6024f6eef2
16 changed files with 67 additions and 57 deletions

View File

@@ -5,11 +5,13 @@ import { combineLatest, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NoResults, NoSendsIcon } from "@bitwarden/assets/svg"; import { NoResults, NoSendsIcon } from "@bitwarden/assets/svg";
import { BrowserPremiumUpgradePromptService } from "@bitwarden/browser/vault/popup/services/browser-premium-upgrade-prompt.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { import {
ButtonModule, ButtonModule,
CalloutModule, CalloutModule,
@@ -39,6 +41,12 @@ export enum SendState {
@Component({ @Component({
templateUrl: "send-v2.component.html", templateUrl: "send-v2.component.html",
providers: [
{
provide: PremiumUpgradePromptService,
useClass: BrowserPremiumUpgradePromptService,
},
],
imports: [ imports: [
CalloutModule, CalloutModule,
PopupPageComponent, PopupPageComponent,

View File

@@ -17,6 +17,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { CipherType, toCipherType } from "@bitwarden/common/vault/enums"; import { CipherType, toCipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
@@ -50,6 +51,7 @@ import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-p
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service"; import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service";
import { BrowserCipherFormGenerationService } from "../../../services/browser-cipher-form-generation.service"; import { BrowserCipherFormGenerationService } from "../../../services/browser-cipher-form-generation.service";
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service"; import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service";
import { import {
fido2PopoutSessionData$, fido2PopoutSessionData$,
@@ -136,6 +138,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }, { provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService },
{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }, { provide: TotpCaptureService, useClass: BrowserTotpCaptureService },
{ provide: CipherFormGenerationService, useClass: BrowserCipherFormGenerationService }, { provide: CipherFormGenerationService, useClass: BrowserCipherFormGenerationService },
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@@ -5,10 +5,10 @@
(click)="openAttachments()" (click)="openAttachments()"
[disabled]="parentFormDisabled" [disabled]="parentFormDisabled"
> >
{{ "attachments" | i18n }} <div class="tw-flex tw-items-center tw-gap-2">
<span *ngIf="!canAccessAttachments" bitBadge variant="success" slot="default-trailing"> {{ "attachments" | i18n }}
{{ "premium" | i18n }} <app-premium-badge></app-premium-badge>
</span> </div>
<ng-container slot="end"> <ng-container slot="end">
<i class="bwi bwi-popout" aria-hidden="true" *ngIf="openAttachmentsInPopout"></i> <i class="bwi bwi-popout" aria-hidden="true" *ngIf="openAttachmentsInPopout"></i>
<i class="bwi bwi-angle-right" aria-hidden="true" *ngIf="!openAttachmentsInPopout"></i> <i class="bwi bwi-angle-right" aria-hidden="true" *ngIf="!openAttachmentsInPopout"></i>

View File

@@ -13,6 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ToastService } from "@bitwarden/components"; import { ToastService } from "@bitwarden/components";
@@ -108,6 +109,12 @@ describe("OpenAttachmentsComponent", () => {
provide: AccountService, provide: AccountService,
useValue: accountService, useValue: accountService,
}, },
{
provide: PremiumUpgradePromptService,
useValue: {
promptForPremium: jest.fn().mockResolvedValue(null),
},
},
], ],
}).compileComponents(); }).compileComponents();
}); });

View File

@@ -6,6 +6,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom, map, switchMap } from "rxjs"; import { firstValueFrom, map, switchMap } from "rxjs";
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { import {
getOrganizationById, getOrganizationById,
@@ -27,7 +28,14 @@ import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/f
@Component({ @Component({
selector: "app-open-attachments", selector: "app-open-attachments",
templateUrl: "./open-attachments.component.html", templateUrl: "./open-attachments.component.html",
imports: [BadgeModule, CommonModule, ItemModule, JslibModule, TypographyModule], imports: [
BadgeModule,
CommonModule,
ItemModule,
JslibModule,
TypographyModule,
PremiumBadgeComponent,
],
}) })
export class OpenAttachmentsComponent implements OnInit { export class OpenAttachmentsComponent implements OnInit {
/** Cipher `id` */ /** Cipher `id` */

View File

@@ -339,16 +339,10 @@
formControlName="enableAutotype" formControlName="enableAutotype"
(change)="saveEnableAutotype()" (change)="saveEnableAutotype()"
/> />
{{ "enableAutotypeTransitionKey" | i18n }} <div class="tw-flex tw-items-center tw-gap-2">
<button {{ "enableAutotypeTransitionKey" | i18n }}
type="button" <app-premium-badge></app-premium-badge>
bitBadge </div>
variant="success"
(click)="openPremiumDialog()"
*ngIf="!hasPremium"
>
{{ "premium" | i18n }}
</button>
</label> </label>
</div> </div>
<small class="help-block" *ngIf="form.value.enableAutotype"> <small class="help-block" *ngIf="form.value.enableAutotype">

View File

@@ -7,6 +7,7 @@ import { RouterModule } from "@angular/router";
import { BehaviorSubject, Observable, Subject, combineLatest, firstValueFrom, of } from "rxjs"; import { BehaviorSubject, Observable, Subject, combineLatest, firstValueFrom, of } from "rxjs";
import { concatMap, map, pairwise, startWith, switchMap, takeUntil, timeout } from "rxjs/operators"; import { concatMap, map, pairwise, startWith, switchMap, takeUntil, timeout } from "rxjs/operators";
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular"; import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -39,6 +40,7 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.e
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { import {
CheckboxModule, CheckboxModule,
DialogService, DialogService,
@@ -51,7 +53,6 @@ import {
SelectModule, SelectModule,
ToastService, ToastService,
TypographyModule, TypographyModule,
BadgeComponent,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management"; import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
@@ -60,17 +61,22 @@ import { SetPinComponent } from "../../auth/components/set-pin.component";
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting"; import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service"; import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service";
import { PremiumComponent } from "../../billing/app/accounts/premium.component";
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service"; import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { DesktopPremiumUpgradePromptService } from "../../services/desktop-premium-upgrade-prompt.service";
import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service"; import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service";
@Component({ @Component({
selector: "app-settings", selector: "app-settings",
templateUrl: "settings.component.html", templateUrl: "settings.component.html",
standalone: true, standalone: true,
providers: [
{
provide: PremiumUpgradePromptService,
useClass: DesktopPremiumUpgradePromptService,
},
],
imports: [ imports: [
BadgeComponent,
CheckboxModule, CheckboxModule,
CommonModule, CommonModule,
FormFieldModule, FormFieldModule,
@@ -87,6 +93,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
TypographyModule, TypographyModule,
VaultTimeoutInputComponent, VaultTimeoutInputComponent,
PermitCipherDetailsPopoverComponent, PermitCipherDetailsPopoverComponent,
PremiumBadgeComponent,
], ],
}) })
export class SettingsComponent implements OnInit, OnDestroy { export class SettingsComponent implements OnInit, OnDestroy {
@@ -134,8 +141,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
pinEnabled$: Observable<boolean> = of(true); pinEnabled$: Observable<boolean> = of(true);
hasPremium: boolean = false;
form = this.formBuilder.group({ form = this.formBuilder.group({
// Security // Security
vaultTimeout: [null as VaultTimeout | null], vaultTimeout: [null as VaultTimeout | null],
@@ -421,9 +426,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
.hasPremiumFromAnySource$(activeAccount.id) .hasPremiumFromAnySource$(activeAccount.id)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe((hasPremium) => { .subscribe((hasPremium) => {
this.hasPremium = hasPremium; if (hasPremium) {
if (this.hasPremium) {
this.form.controls.enableAutotype.enable(); this.form.controls.enableAutotype.enable();
} }
}); });
@@ -892,10 +895,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
} }
} }
async openPremiumDialog() {
await this.dialogService.open(PremiumComponent);
}
async saveEnableAutotype() { async saveEnableAutotype() {
await this.desktopAutotypeService.setAutotypeEnabledState(this.form.value.enableAutotype); await this.desktopAutotypeService.setAutotypeEnabledState(this.form.value.enableAutotype);
} }

View File

@@ -295,7 +295,7 @@ export class AppComponent implements OnInit, OnDestroy {
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef); await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
break; break;
case "openPremium": case "openPremium":
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef); this.dialogService.open(PremiumComponent);
break; break;
case "showFingerprintPhrase": { case "showFingerprintPhrase": {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(

View File

@@ -61,7 +61,7 @@
> >
<b>{{ "premiumPurchase" | i18n }}</b> <b>{{ "premiumPurchase" | i18n }}</b>
</button> </button>
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button> <button type="button" bitDialogClose>{{ "close" | i18n }}</button>
<div class="right" *ngIf="!(isPremium$ | async)"> <div class="right" *ngIf="!(isPremium$ | async)">
<button <button
#refreshBtn #refreshBtn

View File

@@ -42,17 +42,10 @@
(click)="openAttachmentsDialog()" (click)="openAttachmentsDialog()"
[disabled]="formDisabled" [disabled]="formDisabled"
> >
<p class="tw-m-0"> <div class="tw-flex tw-items-center tw-gap-2">
{{ "attachments" | i18n }} {{ "attachments" | i18n }}
<span <app-premium-badge></app-premium-badge>
*ngIf="!(canAccessAttachments$ | async)" </div>
bitBadge
variant="success"
class="tw-ml-2"
>
{{ "premium" | i18n }}
</span>
</p>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</button> </button>
</bit-item> </bit-item>

View File

@@ -13,6 +13,7 @@ import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observabl
import { filter, map, take } from "rxjs/operators"; import { filter, map, take } from "rxjs/operators";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
@@ -101,6 +102,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
I18nPipe, I18nPipe,
ItemModule, ItemModule,
ButtonModule, ButtonModule,
PremiumBadgeComponent,
NavComponent, NavComponent,
VaultFilterModule, VaultFilterModule,
VaultItemsV2Component, VaultItemsV2Component,
@@ -455,7 +457,6 @@ export class VaultV2Component<C extends CipherViewLike>
async openAttachmentsDialog() { async openAttachmentsDialog() {
if (!this.userHasPremiumAccess) { if (!this.userHasPremiumAccess) {
await this.premiumUpgradePromptService.promptForPremium();
return; return;
} }
const dialogRef = AttachmentsV2Component.open(this.dialogService, { const dialogRef = AttachmentsV2Component.open(this.dialogService, {

View File

@@ -452,7 +452,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
const canAccessAttachments = await firstValueFrom(this.canAccessAttachments$); const canAccessAttachments = await firstValueFrom(this.canAccessAttachments$);
if (!canAccessAttachments) { if (!canAccessAttachments) {
await this.premiumUpgradeService.promptForPremium(); await this.premiumUpgradeService.promptForPremium(this.cipher?.organizationId);
return; return;
} }

View File

@@ -1,7 +1,7 @@
import { Component, input, output } from "@angular/core"; import { Component, input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { BadgeModule } from "@bitwarden/components"; import { BadgeModule } from "@bitwarden/components";
@Component({ @Component({
@@ -15,17 +15,11 @@ import { BadgeModule } from "@bitwarden/components";
imports: [BadgeModule, JslibModule], imports: [BadgeModule, JslibModule],
}) })
export class PremiumBadgeComponent { export class PremiumBadgeComponent {
/** Skip sending the premiumRequired message (default: false). */ organizationId = input<string>();
skipMessaging = input(false);
onClick = output();
constructor(private messagingService: MessagingService) {} constructor(private premiumUpgradePromptService: PremiumUpgradePromptService) {}
async promptForPremium() { async promptForPremium() {
this.onClick.emit(); await this.premiumUpgradePromptService.promptForPremium(this.organizationId());
if (this.skipMessaging()) {
return;
}
this.messagingService.send("premiumRequired");
} }
} }

View File

@@ -6,6 +6,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessageSender } from "@bitwarden/common/platform/messaging"; import { MessageSender } from "@bitwarden/common/platform/messaging";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { BadgeModule, I18nMockService } from "@bitwarden/components"; import { BadgeModule, I18nMockService } from "@bitwarden/components";
import { PremiumBadgeComponent } from "./premium-badge.component"; import { PremiumBadgeComponent } from "./premium-badge.component";
@@ -51,6 +52,12 @@ export default {
hasPremiumFromAnySource$: () => of(false), hasPremiumFromAnySource$: () => of(false),
}, },
}, },
{
provide: PremiumUpgradePromptService,
useValue: {
promptForPremium: (orgId?: string) => {},
},
},
], ],
}), }),
], ],

View File

@@ -124,7 +124,7 @@
<bit-label [appTextDrag]="totpCodeCopyObj?.totpCode"> <bit-label [appTextDrag]="totpCodeCopyObj?.totpCode">
<div class="tw-flex tw-items-center tw-gap-3"> <div class="tw-flex tw-items-center tw-gap-3">
{{ "verificationCodeTotp" | i18n }} {{ "verificationCodeTotp" | i18n }}
<app-premium-badge [skipMessaging]="true" (onClick)="getPremium()"></app-premium-badge> <app-premium-badge [organizationId]="cipher.organizationId"></app-premium-badge>
</div> </div>
</bit-label> </bit-label>
<input <input

View File

@@ -107,10 +107,6 @@ export class LoginCredentialsViewComponent implements OnChanges {
} }
} }
async getPremium() {
await this.premiumUpgradeService.promptForPremium(this.cipher.organizationId);
}
async pwToggleValue(passwordVisible: boolean) { async pwToggleValue(passwordVisible: boolean) {
this.passwordRevealed = passwordVisible; this.passwordRevealed = passwordVisible;