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:
parent
59686346d4
commit
d76b5b672c
@ -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));
|
||||
}
|
||||
}
|
@ -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({
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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() {
|
||||
|
@ -0,0 +1,5 @@
|
||||
<bit-nav-item
|
||||
[text]="'sponsoredFamilies' | i18n"
|
||||
route="settings/sponsored-families"
|
||||
*ngIf="showFreeFamilies$ | async"
|
||||
></bit-nav-item>
|
@ -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$;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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([
|
||||
|
Loading…
Reference in New Issue
Block a user