1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-23133] refactor members component (#16703)

* WIP: added new services, refactor members to use billing service and member action service

* replace dialog logic and user logic with service implementations

* WIP

* wip add tests

* add tests, continue refactoring

* clean up

* move BillingConstraintService to billing ownership

* fix import

* fix seat count not updating if feature flag is disabled

* refactor billingMetadata, clean up
This commit is contained in:
Brandon Treston
2025-10-23 11:16:17 -04:00
committed by GitHub
parent 7321e3132b
commit 0691583b50
16 changed files with 2999 additions and 666 deletions

View File

@@ -208,7 +208,7 @@ describe("DefaultOrganizationMetadataService", () => {
}, 10);
});
it("does not trigger refresh when feature flag is disabled", async () => {
it("does trigger refresh when feature flag is disabled", async () => {
featureFlagSubject.next(false);
const mockResponse1 = createMockMetadataResponse(false, 10);
@@ -232,11 +232,10 @@ describe("DefaultOrganizationMetadataService", () => {
service.refreshMetadataCache();
// wait to ensure no additional invocations
await new Promise((resolve) => setTimeout(resolve, 10));
expect(invocationCount).toBe(1);
expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(1);
expect(invocationCount).toBe(2);
expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2);
subscription.unsubscribe();
});

View File

@@ -1,4 +1,4 @@
import { filter, from, merge, Observable, shareReplay, Subject, switchMap } from "rxjs";
import { BehaviorSubject, combineLatest, from, Observable, shareReplay, switchMap } from "rxjs";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
@@ -18,57 +18,56 @@ export class DefaultOrganizationMetadataService implements OrganizationMetadataS
private billingApiService: BillingApiServiceAbstraction,
private configService: ConfigService,
) {}
private refreshMetadataTrigger = new Subject<void>();
private refreshMetadataTrigger = new BehaviorSubject<void>(undefined);
refreshMetadataCache = () => this.refreshMetadataTrigger.next();
refreshMetadataCache = () => {
this.metadataCache.clear();
this.refreshMetadataTrigger.next();
};
getOrganizationMetadata$ = (
organizationId: OrganizationId,
): Observable<OrganizationBillingMetadataResponse> =>
this.configService
.getFeatureFlag$(FeatureFlag.PM25379_UseNewOrganizationMetadataStructure)
.pipe(
switchMap((featureFlagEnabled) => {
return merge(
this.getOrganizationMetadataInternal$(organizationId, featureFlagEnabled),
this.refreshMetadataTrigger.pipe(
filter(() => featureFlagEnabled),
switchMap(() =>
this.getOrganizationMetadataInternal$(organizationId, featureFlagEnabled, true),
),
),
);
}),
);
getOrganizationMetadata$(orgId: OrganizationId): Observable<OrganizationBillingMetadataResponse> {
return combineLatest([
this.refreshMetadataTrigger,
this.configService.getFeatureFlag$(FeatureFlag.PM25379_UseNewOrganizationMetadataStructure),
]).pipe(
switchMap(([_, featureFlagEnabled]) =>
featureFlagEnabled
? this.vNextGetOrganizationMetadataInternal$(orgId)
: this.getOrganizationMetadataInternal$(orgId),
),
);
}
private getOrganizationMetadataInternal$(
organizationId: OrganizationId,
featureFlagEnabled: boolean,
bypassCache: boolean = false,
private vNextGetOrganizationMetadataInternal$(
orgId: OrganizationId,
): Observable<OrganizationBillingMetadataResponse> {
if (!bypassCache && featureFlagEnabled && this.metadataCache.has(organizationId)) {
return this.metadataCache.get(organizationId)!;
const cacheHit = this.metadataCache.get(orgId);
if (cacheHit) {
return cacheHit;
}
const metadata$ = from(this.fetchMetadata(organizationId, featureFlagEnabled)).pipe(
const result = from(this.fetchMetadata(orgId, true)).pipe(
shareReplay({ bufferSize: 1, refCount: false }),
);
if (featureFlagEnabled) {
this.metadataCache.set(organizationId, metadata$);
}
this.metadataCache.set(orgId, result);
return result;
}
return metadata$;
private getOrganizationMetadataInternal$(
organizationId: OrganizationId,
): Observable<OrganizationBillingMetadataResponse> {
return from(this.fetchMetadata(organizationId, false)).pipe(
shareReplay({ bufferSize: 1, refCount: false }),
);
}
private async fetchMetadata(
organizationId: OrganizationId,
featureFlagEnabled: boolean,
): Promise<OrganizationBillingMetadataResponse> {
if (featureFlagEnabled) {
return await this.billingApiService.getOrganizationBillingMetadataVNext(organizationId);
}
return await this.billingApiService.getOrganizationBillingMetadata(organizationId);
return featureFlagEnabled
? await this.billingApiService.getOrganizationBillingMetadataVNext(organizationId)
: await this.billingApiService.getOrganizationBillingMetadata(organizationId);
}
}