mirror of
https://github.com/bitwarden/clients.git
synced 2025-12-05 19:17:06 -06:00
Compare commits
5 Commits
725c39b9a2
...
2a3e13b9b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a3e13b9b3 | ||
|
|
6dc39f3acf | ||
|
|
f782fc0c29 | ||
|
|
1419a21800 | ||
|
|
df400a1b98 |
@@ -5818,8 +5818,8 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
|
||||
@@ -294,19 +294,11 @@ export default class RuntimeBackground {
|
||||
await this.openPopup();
|
||||
break;
|
||||
case VaultMessages.OpenAtRiskPasswords: {
|
||||
if (await this.shouldRejectManyOriginMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.main.openAtRisksPasswordsPage();
|
||||
this.announcePopupOpen();
|
||||
break;
|
||||
}
|
||||
case VaultMessages.OpenBrowserExtensionToUrl: {
|
||||
if (await this.shouldRejectManyOriginMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.main.openTheExtensionToPage(msg.url);
|
||||
this.announcePopupOpen();
|
||||
break;
|
||||
|
||||
@@ -180,7 +180,7 @@ describe("VaultV2Component", () => {
|
||||
const nudgesSvc = {
|
||||
showNudgeSpotlight$: jest.fn().mockImplementation((_type: NudgeType) => of(false)),
|
||||
dismissNudge: jest.fn().mockResolvedValue(undefined),
|
||||
} as Partial<NudgesService>;
|
||||
};
|
||||
|
||||
const dialogSvc = {} as Partial<DialogService>;
|
||||
|
||||
@@ -209,6 +209,10 @@ describe("VaultV2Component", () => {
|
||||
.mockResolvedValue(new Date(Date.now() - 8 * 24 * 60 * 60 * 1000)), // 8 days ago
|
||||
};
|
||||
|
||||
const configSvc = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation((_flag: string) => of(false)),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -256,9 +260,7 @@ describe("VaultV2Component", () => {
|
||||
{ provide: StateProvider, useValue: mock<StateProvider>() },
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
getFeatureFlag$: (_: string) => of(false),
|
||||
},
|
||||
useValue: configSvc,
|
||||
},
|
||||
{
|
||||
provide: SearchService,
|
||||
@@ -453,7 +455,9 @@ describe("VaultV2Component", () => {
|
||||
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
|
||||
(nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) =>
|
||||
configSvc.getFeatureFlag$.mockImplementation((_flag: string) => of(true));
|
||||
|
||||
nudgesSvc.showNudgeSpotlight$.mockImplementation((type: NudgeType) =>
|
||||
of(type === NudgeType.PremiumUpgrade),
|
||||
);
|
||||
|
||||
@@ -482,9 +486,11 @@ describe("VaultV2Component", () => {
|
||||
}));
|
||||
|
||||
it("renders Empty-Vault spotlight when vaultState is Empty and nudge is on", fakeAsync(() => {
|
||||
configSvc.getFeatureFlag$.mockImplementation((_flag: string) => of(false));
|
||||
|
||||
itemsSvc.emptyVault$.next(true);
|
||||
|
||||
(nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => {
|
||||
nudgesSvc.showNudgeSpotlight$.mockImplementation((type: NudgeType) => {
|
||||
return of(type === NudgeType.EmptyVaultNudge);
|
||||
});
|
||||
|
||||
|
||||
@@ -137,6 +137,10 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
FeatureFlag.VaultLoadingSkeletons,
|
||||
);
|
||||
|
||||
protected premiumSpotlightFeatureFlag$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.BrowserPremiumSpotlight,
|
||||
);
|
||||
|
||||
private showPremiumNudgeSpotlight$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.nudgesService.showNudgeSpotlight$(NudgeType.PremiumUpgrade, userId)),
|
||||
);
|
||||
@@ -164,6 +168,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
);
|
||||
|
||||
protected showPremiumSpotlight$ = combineLatest([
|
||||
this.premiumSpotlightFeatureFlag$,
|
||||
this.showPremiumNudgeSpotlight$,
|
||||
this.showHasItemsVaultSpotlight$,
|
||||
this.hasPremium$,
|
||||
@@ -171,8 +176,13 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.accountAgeInDays$,
|
||||
]).pipe(
|
||||
map(
|
||||
([showPremiumNudge, showHasItemsNudge, hasPremium, count, age]) =>
|
||||
showPremiumNudge && !showHasItemsNudge && !hasPremium && count >= 5 && age >= 7,
|
||||
([featureFlagEnabled, showPremiumNudge, showHasItemsNudge, hasPremium, count, age]) =>
|
||||
featureFlagEnabled &&
|
||||
showPremiumNudge &&
|
||||
!showHasItemsNudge &&
|
||||
!hasPremium &&
|
||||
count >= 5 &&
|
||||
age >= 7,
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
@@ -4212,8 +4212,8 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
<app-side-nav variant="secondary" *ngIf="organization$ | async as organization">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'adminConsole' | i18n"></bit-nav-logo>
|
||||
<org-switcher [filter]="orgFilter" [hideNewButton]="hideNewOrgButton$ | async"></org-switcher>
|
||||
<bit-nav-item
|
||||
icon="bwi-dashboard"
|
||||
*ngIf="organization.useAccessIntelligence && organization.canAccessReports"
|
||||
[text]="'accessIntelligence' | i18n"
|
||||
route="access-intelligence"
|
||||
></bit-nav-item>
|
||||
|
||||
@if (canShowAccessIntelligenceTab(organization)) {
|
||||
<bit-nav-item
|
||||
icon="bwi-dashboard"
|
||||
[text]="'accessIntelligence' | i18n"
|
||||
route="access-intelligence"
|
||||
></bit-nav-item>
|
||||
}
|
||||
|
||||
<bit-nav-item
|
||||
icon="bwi-collection-shared"
|
||||
[text]="'collections' | i18n"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, inject, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { combineLatest, filter, map, Observable, switchMap, withLatestFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AdminConsoleLogo } from "@bitwarden/assets/svg";
|
||||
import {
|
||||
canAccessAccessIntelligence,
|
||||
canAccessBillingTab,
|
||||
canAccessGroupsTab,
|
||||
canAccessMembersTab,
|
||||
@@ -72,16 +73,14 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
protected subscriber$: Observable<NonIndividualSubscriber>;
|
||||
protected getTaxIdWarning$: () => Observable<TaxIdWarningType | null>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private policyService: PolicyService,
|
||||
private providerService: ProviderService,
|
||||
private accountService: AccountService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
) {}
|
||||
private route = inject(ActivatedRoute);
|
||||
private organizationService = inject(OrganizationService);
|
||||
private platformUtilsService = inject(PlatformUtilsService);
|
||||
private policyService = inject(PolicyService);
|
||||
private providerService = inject(ProviderService);
|
||||
private accountService = inject(AccountService);
|
||||
private freeFamiliesPolicyService = inject(FreeFamiliesPolicyService);
|
||||
private organizationWarningsService = inject(OrganizationWarningsService);
|
||||
|
||||
async ngOnInit() {
|
||||
document.body.classList.remove("layout_frontend");
|
||||
@@ -172,6 +171,10 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
return canAccessBillingTab(organization);
|
||||
}
|
||||
|
||||
canShowAccessIntelligenceTab(organization: Organization): boolean {
|
||||
return canAccessAccessIntelligence(organization);
|
||||
}
|
||||
|
||||
getReportTabLabel(organization: Organization): string {
|
||||
return organization.useEvents ? "reporting" : "reports";
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div>
|
||||
@if (premiumCardData$ | async; as premiumData) {
|
||||
<billing-pricing-card
|
||||
[tagline]="'planDescPremium' | i18n"
|
||||
[tagline]="'advancedOnlineSecurity' | i18n"
|
||||
[price]="{ amount: premiumData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'primary', text: ('upgradeToPremium' | i18n) }"
|
||||
[features]="premiumData.features"
|
||||
|
||||
@@ -11929,8 +11929,8 @@
|
||||
"familiesMembership": {
|
||||
"message": "Families membership"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"planDescFamiliesV2": {
|
||||
"message": "Premium security for your family"
|
||||
|
||||
@@ -2,7 +2,10 @@ import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { authGuard } from "@bitwarden/angular/auth/guards";
|
||||
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
canAccessAccessIntelligence,
|
||||
canAccessSettingsTab,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard";
|
||||
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
|
||||
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
|
||||
@@ -79,7 +82,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "access-intelligence",
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessReports)],
|
||||
canActivate: [organizationPermissionsGuard(canAccessAccessIntelligence)],
|
||||
loadChildren: () =>
|
||||
import("../../dirt/access-intelligence/access-intelligence.module").then(
|
||||
(m) => m.AccessIntelligenceModule,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessAccessIntelligence } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
|
||||
|
||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
@@ -8,7 +9,7 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canAccessReports)],
|
||||
canActivate: [organizationPermissionsGuard(canAccessAccessIntelligence)],
|
||||
component: RiskInsightsComponent,
|
||||
data: {
|
||||
titleId: "accessIntelligence",
|
||||
|
||||
@@ -41,6 +41,18 @@ export function canAccessBillingTab(org: Organization): boolean {
|
||||
return org.isOwner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Intelligence is only available to:
|
||||
* - Enterprise organizations
|
||||
* - Users in those organizations with report access
|
||||
*
|
||||
* @param org The organization to verify access
|
||||
* @returns If true can access the Access Intelligence feature
|
||||
*/
|
||||
export function canAccessAccessIntelligence(org: Organization): boolean {
|
||||
return org.canUseAccessIntelligence && org.canAccessReports;
|
||||
}
|
||||
|
||||
export function canAccessOrgAdmin(org: Organization): boolean {
|
||||
// Admin console can only be accessed by Owners for disabled organizations
|
||||
if (!org.enabled && !org.isOwner) {
|
||||
|
||||
@@ -400,4 +400,8 @@ export class Organization {
|
||||
this.permissions.accessEventLogs)
|
||||
);
|
||||
}
|
||||
|
||||
get canUseAccessIntelligence() {
|
||||
return this.productTierType === ProductTierType.Enterprise;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
||||
return "Custom";
|
||||
|
||||
// Plan descriptions
|
||||
case "planDescPremium":
|
||||
case "advancedOnlineSecurity":
|
||||
return "Premium plan description";
|
||||
case "planDescFamiliesV2":
|
||||
return "Families plan description";
|
||||
@@ -393,7 +393,7 @@ describe("DefaultSubscriptionPricingService", () => {
|
||||
});
|
||||
|
||||
expect(i18nService.t).toHaveBeenCalledWith("premium");
|
||||
expect(i18nService.t).toHaveBeenCalledWith("planDescPremium");
|
||||
expect(i18nService.t).toHaveBeenCalledWith("advancedOnlineSecurity");
|
||||
expect(i18nService.t).toHaveBeenCalledWith("planNameFamilies");
|
||||
expect(i18nService.t).toHaveBeenCalledWith("planDescFamiliesV2");
|
||||
expect(i18nService.t).toHaveBeenCalledWith("builtInAuthenticator");
|
||||
|
||||
@@ -124,7 +124,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer
|
||||
map((premiumPrices) => ({
|
||||
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||
name: this.i18nService.t("premium"),
|
||||
description: this.i18nService.t("planDescPremium"),
|
||||
description: this.i18nService.t("advancedOnlineSecurity"),
|
||||
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||
passwordManager: {
|
||||
type: "standalone",
|
||||
|
||||
@@ -63,6 +63,7 @@ export enum FeatureFlag {
|
||||
AutofillConfirmation = "pm-25083-autofill-confirm-from-search",
|
||||
RiskInsightsForPremium = "pm-23904-risk-insights-for-premium",
|
||||
VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders",
|
||||
BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight",
|
||||
|
||||
/* Platform */
|
||||
IpcChannelFramework = "ipc-channel-framework",
|
||||
@@ -117,6 +118,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.AutofillConfirmation]: FALSE,
|
||||
[FeatureFlag.RiskInsightsForPremium]: FALSE,
|
||||
[FeatureFlag.VaultLoadingSkeletons]: FALSE,
|
||||
[FeatureFlag.BrowserPremiumSpotlight]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE,
|
||||
|
||||
@@ -51,10 +51,10 @@ describe("Default task service", () => {
|
||||
mockGetAllOrgs$.mockReturnValue(
|
||||
new BehaviorSubject([
|
||||
{
|
||||
useAccessIntelligence: false,
|
||||
canUseAccessIntelligence: false,
|
||||
},
|
||||
{
|
||||
useAccessIntelligence: true,
|
||||
canUseAccessIntelligence: true,
|
||||
},
|
||||
] as Organization[]),
|
||||
);
|
||||
@@ -70,10 +70,10 @@ describe("Default task service", () => {
|
||||
mockGetAllOrgs$.mockReturnValue(
|
||||
new BehaviorSubject([
|
||||
{
|
||||
useAccessIntelligence: false,
|
||||
canUseAccessIntelligence: false,
|
||||
},
|
||||
{
|
||||
useAccessIntelligence: false,
|
||||
canUseAccessIntelligence: false,
|
||||
},
|
||||
] as Organization[]),
|
||||
);
|
||||
@@ -91,17 +91,17 @@ describe("Default task service", () => {
|
||||
mockGetAllOrgs$.mockReturnValue(
|
||||
new BehaviorSubject([
|
||||
{
|
||||
useAccessIntelligence: true,
|
||||
canUseAccessIntelligence: true,
|
||||
},
|
||||
] as Organization[]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return an empty array if tasks are not enabled", async () => {
|
||||
it("should return no tasks if not present and canUserAccessIntelligence is false", async () => {
|
||||
mockGetAllOrgs$.mockReturnValue(
|
||||
new BehaviorSubject([
|
||||
{
|
||||
useAccessIntelligence: false,
|
||||
canUseAccessIntelligence: false,
|
||||
},
|
||||
] as Organization[]),
|
||||
);
|
||||
@@ -111,7 +111,6 @@ describe("Default task service", () => {
|
||||
const result = await firstValueFrom(tasks$("user-id" as UserId));
|
||||
|
||||
expect(result.length).toBe(0);
|
||||
expect(mockApiSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fetch tasks from the API when the state is null", async () => {
|
||||
@@ -163,17 +162,17 @@ describe("Default task service", () => {
|
||||
mockGetAllOrgs$.mockReturnValue(
|
||||
new BehaviorSubject([
|
||||
{
|
||||
useAccessIntelligence: true,
|
||||
canUseAccessIntelligence: true,
|
||||
},
|
||||
] as Organization[]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return an empty array if tasks are not enabled", async () => {
|
||||
it("should return no tasks if not present and canUserAccessIntelligence is false", async () => {
|
||||
mockGetAllOrgs$.mockReturnValue(
|
||||
new BehaviorSubject([
|
||||
{
|
||||
useAccessIntelligence: false,
|
||||
canUseAccessIntelligence: false,
|
||||
},
|
||||
] as Organization[]),
|
||||
);
|
||||
@@ -183,7 +182,6 @@ describe("Default task service", () => {
|
||||
const result = await firstValueFrom(pendingTasks$("user-id" as UserId));
|
||||
|
||||
expect(result.length).toBe(0);
|
||||
expect(mockApiSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should filter tasks to only pending tasks", async () => {
|
||||
|
||||
@@ -48,7 +48,7 @@ export class DefaultTaskService implements TaskService {
|
||||
|
||||
tasksEnabled$ = perUserCache$((userId) => {
|
||||
return this.organizationService.organizations$(userId).pipe(
|
||||
map((orgs) => orgs.some((o) => o.useAccessIntelligence)),
|
||||
map((orgs) => orgs.some((o) => o.canUseAccessIntelligence)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user