1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-04 18:37:45 +01:00

[PM-13347] Web app impacts on the Remove Bitwarden Families policy (#12056)

* Changes for web impact by the policy

* Changes to address PR comments

* refactoring changes from pr comments

* Resolve the complex conditionals comment

* resolve the complex conditionals comment

* Resolve the pr comments on user layout

* revert on wanted change

* Refactor and move logic and template into its own component

* Move to a folder owned by the Billing team
This commit is contained in:
cyprain-okeke 2024-11-28 15:59:05 +01:00 committed by GitHub
parent 59686346d4
commit d76b5b672c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 231 additions and 17 deletions

View File

@ -0,0 +1,125 @@
import { Injectable } from "@angular/core";
import { combineLatest, filter, from, map, Observable, of, switchMap } from "rxjs";
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 { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
interface EnterpriseOrgStatus {
isFreeFamilyPolicyEnabled: boolean;
belongToOneEnterpriseOrgs: boolean;
belongToMultipleEnterpriseOrgs: boolean;
}
@Injectable({ providedIn: "root" })
export class FreeFamiliesPolicyService {
protected enterpriseOrgStatus: EnterpriseOrgStatus = {
isFreeFamilyPolicyEnabled: false,
belongToOneEnterpriseOrgs: false,
belongToMultipleEnterpriseOrgs: false,
};
constructor(
private policyService: PolicyService,
private organizationService: OrganizationService,
private configService: ConfigService,
) {}
get showFreeFamilies$(): Observable<boolean> {
return this.isFreeFamilyFlagEnabled$.pipe(
switchMap((isFreeFamilyFlagEnabled) =>
isFreeFamilyFlagEnabled
? this.getFreeFamiliesVisibility$()
: this.organizationService.canManageSponsorships$,
),
);
}
private getFreeFamiliesVisibility$(): Observable<boolean> {
return combineLatest([
this.checkEnterpriseOrganizationsAndFetchPolicy(),
this.organizationService.canManageSponsorships$,
]).pipe(
map(([orgStatus, canManageSponsorships]) =>
this.shouldShowFreeFamilyLink(orgStatus, canManageSponsorships),
),
);
}
private shouldShowFreeFamilyLink(
orgStatus: EnterpriseOrgStatus | null,
canManageSponsorships: boolean,
): boolean {
if (!orgStatus) {
return false;
}
const { belongToOneEnterpriseOrgs, isFreeFamilyPolicyEnabled } = orgStatus;
return canManageSponsorships && !(belongToOneEnterpriseOrgs && isFreeFamilyPolicyEnabled);
}
checkEnterpriseOrganizationsAndFetchPolicy(): Observable<EnterpriseOrgStatus> {
return this.organizationService.organizations$.pipe(
filter((organizations) => Array.isArray(organizations) && organizations.length > 0),
switchMap((organizations) => this.fetchEnterpriseOrganizationPolicy(organizations)),
);
}
private fetchEnterpriseOrganizationPolicy(
organizations: Organization[],
): Observable<EnterpriseOrgStatus> {
const { belongToOneEnterpriseOrgs, belongToMultipleEnterpriseOrgs } =
this.evaluateEnterpriseOrganizations(organizations);
if (!belongToOneEnterpriseOrgs) {
return of({
isFreeFamilyPolicyEnabled: false,
belongToOneEnterpriseOrgs,
belongToMultipleEnterpriseOrgs,
});
}
const organizationId = this.getOrganizationIdForOneEnterprise(organizations);
if (!organizationId) {
return of({
isFreeFamilyPolicyEnabled: false,
belongToOneEnterpriseOrgs,
belongToMultipleEnterpriseOrgs,
});
}
return this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy).pipe(
map((policies) => ({
isFreeFamilyPolicyEnabled: policies.some(
(policy) => policy.organizationId === organizationId && policy.enabled,
),
belongToOneEnterpriseOrgs,
belongToMultipleEnterpriseOrgs,
})),
);
}
private evaluateEnterpriseOrganizations(organizations: any[]): {
belongToOneEnterpriseOrgs: boolean;
belongToMultipleEnterpriseOrgs: boolean;
} {
const enterpriseOrganizations = organizations.filter((org) => org.canManageSponsorships);
const count = enterpriseOrganizations.length;
return {
belongToOneEnterpriseOrgs: count === 1,
belongToMultipleEnterpriseOrgs: count > 1,
};
}
private getOrganizationIdForOneEnterprise(organizations: any[]): string | null {
const enterpriseOrganizations = organizations.filter((org) => org.canManageSponsorships);
return enterpriseOrganizations.length === 1 ? enterpriseOrganizations[0].id : null;
}
private get isFreeFamilyFlagEnabled$(): Observable<boolean> {
return from(this.configService.getFeatureFlag(FeatureFlag.DisableFreeFamiliesSponsorship));
}
}

View File

@ -8,13 +8,17 @@ import {
AsyncValidatorFn,
ValidationErrors,
} from "@angular/forms";
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { combineLatest, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { PlanSponsorshipType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@ -31,6 +35,7 @@ interface RequestSponsorshipForm {
})
export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
loading = false;
isFreeFamilyFlagEnabled: boolean;
availableSponsorshipOrgs$: Observable<Organization[]>;
activeSponsorshipOrgs$: Observable<Organization[]>;
@ -53,6 +58,8 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
private formBuilder: FormBuilder,
private accountService: AccountService,
private toastService: ToastService,
private configService: ConfigService,
private policyService: PolicyService,
) {
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
selectedSponsorshipOrgId: new FormControl("", {
@ -72,10 +79,34 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)),
this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.DisableFreeFamiliesSponsorship,
);
if (this.isFreeFamilyFlagEnabled) {
this.availableSponsorshipOrgs$ = combineLatest([
this.organizationService.organizations$,
this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy),
]).pipe(
map(([organizations, policies]) =>
organizations
.filter((org) => org.familySponsorshipAvailable)
.map((org) => ({
organization: org,
isPolicyEnabled: policies.some(
(policy) => policy.organizationId === org.id && policy.enabled,
),
}))
.filter(({ isPolicyEnabled }) => !isPolicyEnabled)
.map(({ organization }) => organization),
),
);
} else {
this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)),
);
}
this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
if (orgs.length === 1) {
this.sponsorshipForm.patchValue({

View File

@ -18,7 +18,11 @@
<button
type="button"
bitMenuItem
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
*ngIf="
!isSelfHosted &&
!sponsoringOrg.familySponsorshipValidUntil &&
!(isFreeFamilyPolicyEnabled$ | async)
"
(click)="resendEmail()"
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
>

View File

@ -1,9 +1,13 @@
import { formatDate } from "@angular/common";
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -21,7 +25,8 @@ export class SponsoringOrgRowComponent implements OnInit {
statusMessage = "loading";
statusClass: "tw-text-success" | "tw-text-danger" = "tw-text-success";
isFreeFamilyPolicyEnabled$: Observable<boolean>;
isFreeFamilyFlagEnabled: boolean;
private locale = "";
constructor(
@ -31,6 +36,8 @@ export class SponsoringOrgRowComponent implements OnInit {
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService,
private toastService: ToastService,
private configService: ConfigService,
private policyService: PolicyService,
) {}
async ngOnInit() {
@ -42,6 +49,23 @@ export class SponsoringOrgRowComponent implements OnInit {
this.sponsoringOrg.familySponsorshipValidUntil,
this.sponsoringOrg.familySponsorshipLastSyncDate,
);
this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.DisableFreeFamiliesSponsorship,
);
if (this.isFreeFamilyFlagEnabled) {
this.isFreeFamilyPolicyEnabled$ = this.policyService
.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy)
.pipe(
map(
(policies) =>
Array.isArray(policies) &&
policies.some(
(policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled,
),
),
);
}
}
async revokeSponsorship() {

View File

@ -0,0 +1,5 @@
<bit-nav-item
[text]="'sponsoredFamilies' | i18n"
route="settings/sponsored-families"
*ngIf="showFreeFamilies$ | async"
></bit-nav-item>

View File

@ -0,0 +1,22 @@
import { Component } from "@angular/core";
import { Observable } from "rxjs";
import { NavigationModule } from "@bitwarden/components";
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
import { BillingSharedModule } from "./billing-shared.module";
@Component({
selector: "billing-free-families-nav-item",
templateUrl: "./billing-free-families-nav-item.component.html",
standalone: true,
imports: [NavigationModule, BillingSharedModule],
})
export class BillingFreeFamiliesNavItemComponent {
showFreeFamilies$: Observable<boolean>;
constructor(private freeFamiliesPolicyService: FreeFamiliesPolicyService) {
this.showFreeFamilies$ = this.freeFamiliesPolicyService.showFreeFamilies$;
}
}

View File

@ -24,11 +24,7 @@
[text]="'emergencyAccess' | i18n"
route="settings/emergency-access"
></bit-nav-item>
<bit-nav-item
[text]="'sponsoredFamilies' | i18n"
route="settings/sponsored-families"
*ngIf="hasFamilySponsorshipAvailable$ | async"
></bit-nav-item>
<billing-free-families-nav-item></billing-free-families-nav-item>
</bit-nav-group>
</app-side-nav>

View File

@ -1,16 +1,17 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Observable, combineLatest, concatMap } from "rxjs";
import { Observable, concatMap, combineLatest } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { IconModule } from "@bitwarden/components";
import { BillingFreeFamiliesNavItemComponent } from "../billing/shared/billing-free-families-nav-item.component";
import { PasswordManagerLogo } from "./password-manager-logo";
import { WebLayoutModule } from "./web-layout.module";
@ -18,16 +19,24 @@ import { WebLayoutModule } from "./web-layout.module";
selector: "app-user-layout",
templateUrl: "user-layout.component.html",
standalone: true,
imports: [CommonModule, RouterModule, JslibModule, WebLayoutModule, IconModule],
imports: [
CommonModule,
RouterModule,
JslibModule,
WebLayoutModule,
IconModule,
BillingFreeFamiliesNavItemComponent,
],
})
export class UserLayoutComponent implements OnInit {
protected readonly logo = PasswordManagerLogo;
isFreeFamilyFlagEnabled: boolean;
protected hasFamilySponsorshipAvailable$: Observable<boolean>;
protected showSponsoredFamilies$: Observable<boolean>;
protected showSubscription$: Observable<boolean>;
constructor(
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private apiService: ApiService,
private syncService: SyncService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
@ -38,8 +47,6 @@ export class UserLayoutComponent implements OnInit {
await this.syncService.fullSync(false);
this.hasFamilySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
// We want to hide the subscription menu for organizations that provide premium.
// Except if the user has premium personally or has a billing history.
this.showSubscription$ = combineLatest([