diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts index a9133d220c3..a0b71e598f6 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts @@ -3,10 +3,12 @@ import * as rxjs from "rxjs"; import { of } from "rxjs"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; import { DialogRef, DialogService } from "@bitwarden/components"; import { @@ -22,7 +24,9 @@ describe("UnifiedUpgradePromptService", () => { const mockConfigService = mock(); const mockBillingService = mock(); const mockVaultProfileService = mock(); + const mockSyncService = mock(); const mockDialogService = mock(); + const mockOrganizationService = mock(); const mockDialogOpen = jest.spyOn(UnifiedUpgradeDialogComponent, "open"); /** @@ -50,7 +54,9 @@ describe("UnifiedUpgradePromptService", () => { mockConfigService, mockBillingService, mockVaultProfileService, + mockSyncService, mockDialogService, + mockOrganizationService, ); } @@ -81,6 +87,12 @@ describe("UnifiedUpgradePromptService", () => { mockReset(mockConfigService); mockReset(mockBillingService); mockReset(mockVaultProfileService); + mockReset(mockSyncService); + mockReset(mockOrganizationService); + + // Mock sync service methods + mockSyncService.fullSync.mockResolvedValue(true); + mockSyncService.lastSync$.mockReturnValue(of(new Date())); }); it("should not show dialog when feature flag is disabled", async () => { // Arrange @@ -97,6 +109,21 @@ describe("UnifiedUpgradePromptService", () => { // Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(true)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); + setupTestService(); + + // Act + const result = await sut.displayUpgradePromptConditionally(); + + // Assert + expect(result).toBeNull(); + }); + + it("should not show dialog when user has any organization membership", async () => { + // Arrange + mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); + mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([{ id: "org1" } as any])); setupTestService(); // Act @@ -110,6 +137,7 @@ describe("UnifiedUpgradePromptService", () => { // Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const oldDate = new Date(); oldDate.setMinutes(oldDate.getMinutes() - 10); // 10 minutes old mockVaultProfileService.getProfileCreationDate.mockResolvedValue(oldDate); @@ -126,6 +154,7 @@ describe("UnifiedUpgradePromptService", () => { //Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const recentDate = new Date(); recentDate.setMinutes(recentDate.getMinutes() - 3); // 3 minutes old mockVaultProfileService.getProfileCreationDate.mockResolvedValue(recentDate); @@ -159,6 +188,7 @@ describe("UnifiedUpgradePromptService", () => { // Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); mockVaultProfileService.getProfileCreationDate.mockResolvedValue(null); setupTestService(); diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts index e90f696cfb5..8dd7f31275c 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts @@ -1,12 +1,14 @@ import { Injectable } from "@angular/core"; -import { combineLatest, firstValueFrom } from "rxjs"; -import { switchMap, take } from "rxjs/operators"; +import { combineLatest, firstValueFrom, timeout } from "rxjs"; +import { filter, switchMap, take } from "rxjs/operators"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; import { DialogRef, DialogService } from "@bitwarden/components"; import { @@ -24,7 +26,9 @@ export class UnifiedUpgradePromptService { private configService: ConfigService, private billingAccountProfileStateService: BillingAccountProfileStateService, private vaultProfileService: VaultProfileService, + private syncService: SyncService, private dialogService: DialogService, + private organizationService: OrganizationService, ) {} private shouldShowPrompt$ = combineLatest([ @@ -40,6 +44,19 @@ export class UnifiedUpgradePromptService { return false; } + // Wait for sync to complete to ensure organizations are fully loaded + // Also force a sync to ensure we have the latest data + await this.syncService.fullSync(false); + + // Wait for the sync to complete with timeout to prevent hanging + await firstValueFrom( + this.syncService.lastSync$(account.id).pipe( + filter((lastSync) => lastSync !== null), + take(1), + timeout(30000), // 30 second timeout + ), + ); + // Check if user has premium const hasPremium = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), @@ -50,12 +67,25 @@ export class UnifiedUpgradePromptService { return false; } + // Check if user has any organization membership (any status including pending) + // Try using memberOrganizations$ which might have different filtering logic + const memberOrganizations = await firstValueFrom( + this.organizationService.memberOrganizations$(account.id), + ); + + const hasOrganizations = memberOrganizations.length > 0; + + // Early return if user has any organization status + if (hasOrganizations) { + return false; + } + // Check profile age only if needed const isProfileLessThanFiveMinutesOld = await this.isProfileLessThanFiveMinutesOld( account.id, ); - return isFlagEnabled && !hasPremium && isProfileLessThanFiveMinutesOld; + return isFlagEnabled && !hasPremium && !hasOrganizations && isProfileLessThanFiveMinutesOld; }), take(1), ); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 32f35375542..7ea1d02110d 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -620,7 +620,7 @@ export class VaultComponent implements OnInit, OnDestr this.changeDetectorRef.markForCheck(); }, ); - await this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); + void this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); } ngOnDestroy() {