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 5ca8dfd5ad2..136845cd31f 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -32,6 +32,7 @@ import { Unassigned, } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; import { NoResults, DeactivatedOrg, @@ -52,7 +53,9 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -103,6 +106,10 @@ import { CollectionDialogTabType, openCollectionDialog, } from "../../admin-console/organizations/shared/components/collection-dialog"; +import { + UnifiedUpgradeDialogComponent, + UnifiedUpgradeDialogResult, +} from "../../billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component"; import { SharedModule } from "../../shared/shared.module"; import { AssignCollectionsWebComponent } from "../components/assign-collections"; import { @@ -205,6 +212,8 @@ export class VaultComponent implements OnInit, OnDestr private vaultItemDialogRef?: DialogRef | undefined; protected showAddCipherBtn: boolean = false; + private unifiedDialogRef?: DialogRef; + organizations$ = this.accountService.activeAccount$ .pipe(map((a) => a?.id)) .pipe(switchMap((id) => this.organizationService.organizations$(id))); @@ -306,6 +315,8 @@ export class VaultComponent implements OnInit, OnDestr private restrictedItemTypesService: RestrictedItemTypesService, private cipherArchiveService: CipherArchiveService, private organizationWarningsService: OrganizationWarningsService, + private vaultProfileService: VaultProfileService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -606,6 +617,58 @@ export class VaultComponent implements OnInit, OnDestr this.changeDetectorRef.markForCheck(); }, ); + + const showUpgradeDialog$: Observable = combineLatest([ + this.accountService.activeAccount$, + this.configService.getFeatureFlag$(FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog), + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), + ]).pipe( + switchMap(async ([account, isFlagEnabled, hasPremium]) => { + // If user already has premium, no need to check profile age + if (hasPremium || !isFlagEnabled) { + return false; + } + const isProfileLessThanFiveMinutesOld = await this.isProfileLessThanFiveMinutesOld( + account?.id, + ); + + return isFlagEnabled && !hasPremium && isProfileLessThanFiveMinutesOld; + }), + ); + + showUpgradeDialog$.pipe(takeUntil(this.destroy$)).subscribe((show) => { + if (show && this.unifiedDialogRef == null) { + this.launchUpgradeDialogFlow().catch((err) => { + this.logService.error("Error in upgrade dialog flow", err); + }); + } + }); + } + + private async isProfileLessThanFiveMinutesOld(userId: string): Promise { + const createdAtDate = await this.vaultProfileService.getProfileCreationDate(userId); + if (!createdAtDate) { + return false; + } + const createdAtInMs = createdAtDate.getTime(); + const nowInMs = new Date().getTime(); + + const differenceInMs = nowInMs - createdAtInMs; + const msInAMinute = 1000 * 60; // Milliseconds in a minute for conversion 1 minute = 60 seconds * 1000 ms + const differenceInMinutes = Math.round(differenceInMs / msInAMinute); + + return differenceInMinutes <= 5; + } + + private async launchUpgradeDialogFlow(): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); + + this.unifiedDialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, { + data: { account: account }, + }); + + await lastValueFrom(this.unifiedDialogRef.closed); + this.unifiedDialogRef = undefined; } ngOnDestroy() {