From d76b5b672cf08675e94f81b4033be59c4a3c6e8f Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:59:05 +0100 Subject: [PATCH] [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 --- .../services/free-families-policy.service.ts | 125 ++++++++++++++++++ .../settings/sponsored-families.component.ts | 37 +++++- .../sponsoring-org-row.component.html | 6 +- .../settings/sponsoring-org-row.component.ts | 28 +++- ...ling-free-families-nav-item.component.html | 5 + ...illing-free-families-nav-item.component.ts | 22 +++ .../app/layouts/user-layout.component.html | 6 +- .../src/app/layouts/user-layout.component.ts | 19 ++- 8 files changed, 231 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/app/billing/services/free-families-policy.service.ts create mode 100644 apps/web/src/app/billing/shared/billing-free-families-nav-item.component.html create mode 100644 apps/web/src/app/billing/shared/billing-free-families-nav-item.component.ts diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts new file mode 100644 index 0000000000..cc53e0a32b --- /dev/null +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -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 { + return this.isFreeFamilyFlagEnabled$.pipe( + switchMap((isFreeFamilyFlagEnabled) => + isFreeFamilyFlagEnabled + ? this.getFreeFamiliesVisibility$() + : this.organizationService.canManageSponsorships$, + ), + ); + } + + private getFreeFamiliesVisibility$(): Observable { + 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 { + return this.organizationService.organizations$.pipe( + filter((organizations) => Array.isArray(organizations) && organizations.length > 0), + switchMap((organizations) => this.fetchEnterpriseOrganizationPolicy(organizations)), + ); + } + + private fetchEnterpriseOrganizationPolicy( + organizations: Organization[], + ): Observable { + 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 { + return from(this.configService.getFeatureFlag(FeatureFlag.DisableFreeFamiliesSponsorship)); + } +} diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index c098b6044c..f49e7acce2 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -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; activeSponsorshipOrgs$: Observable; @@ -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({ 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({ diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.html b/apps/web/src/app/billing/settings/sponsoring-org-row.component.html index b07cbbfad1..eeeaa25604 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.html +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.html @@ -18,7 +18,11 @@