diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 5fefbf9ddff..a05b09e139e 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -330,6 +330,20 @@ export class AppComponent implements OnInit, OnDestroy { } break; } + case "upgradeOrganization": { + const upgradeConfirmed = await this.dialogService.openSimpleDialog({ + title: { key: "upgradeOrganization" }, + content: { key: "upgradeOrganizationDesc" }, + acceptButtonText: { key: "learnMore" }, + type: "info", + }); + if (upgradeConfirmed) { + this.platformUtilsService.launchUri( + "https://bitwarden.com/help/upgrade-from-individual-to-org/", + ); + } + break; + } case "emailVerificationRequired": { const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({ title: { key: "emailVerificationRequired" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 1a02e5db4e7..bca12f16a7d 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3474,5 +3474,14 @@ }, "changeAcctEmail": { "message": "Change account email" + }, + "organizationUpgradeRequired": { + "message": "Organization upgrade required" + }, + "upgradeOrganization": { + "message": "Upgrade organization" + }, + "upgradeOrganizationDesc": { + "message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features." } } diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index 59e609312d7..f589ba53046 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -186,6 +186,16 @@

+
+
+ {{ "verificationCodeTotp" | i18n }} + + {{ "organizationUpgradeRequired" | i18n }} + + +
+
diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index 23b85eceda2..d3e8fff3495 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -157,4 +157,10 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro this.messagingService.send("premiumRequired"); } } + + upgradeOrganization() { + this.messagingService.send("upgradeOrganization", { + organizationId: this.cipher.organizationId, + }); + } } diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 56db7dc88da..916c845e9d3 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -66,7 +66,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On protected messagingService: MessagingService, eventCollectionService: EventCollectionService, protected policyService: PolicyService, - organizationService: OrganizationService, + protected organizationService: OrganizationService, logService: LogService, passwordRepromptService: PasswordRepromptService, dialogService: DialogService, @@ -307,7 +307,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On this.cipher.type === CipherType.Login && this.cipher.login.totp && this.organization?.productTierType != ProductTierType.Free && - (this.cipher.organizationUseTotp || this.canAccessPremium) + ((this.canAccessPremium && this.cipher.organizationId == null) || + this.cipher.organizationUseTotp) ); } diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 8d286e0a3f9..bf2e68b71cd 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -128,7 +128,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected logService: LogService, protected passwordRepromptService: PasswordRepromptService, - private organizationService: OrganizationService, + protected organizationService: OrganizationService, protected dialogService: DialogService, protected win: Window, protected datePipe: DatePipe, diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index fc12aeff2f2..724a1507be1 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -65,6 +65,7 @@ export class ViewComponent implements OnDestroy, OnInit { showPrivateKey: boolean; canAccessPremium: boolean; showPremiumRequiredTotp: boolean; + showUpgradeRequiredTotp: boolean; totpCode: string; totpCodeFormatted: string; totpDash: number; @@ -151,22 +152,25 @@ export class ViewComponent implements OnDestroy, OnInit { this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); this.showPremiumRequiredTotp = - this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; + this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId; this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ this.collectionId as CollectionId, ]); + this.showUpgradeRequiredTotp = + this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp; + if (this.cipher.folderId) { this.folder = await ( await firstValueFrom(this.folderService.folderViews$(activeUserId)) ).find((f) => f.id == this.cipher.folderId); } - if ( - this.cipher.type === CipherType.Login && - this.cipher.login.totp && - (cipher.organizationUseTotp || this.canAccessPremium) - ) { + const canGenerateTotp = this.cipher.organizationId + ? this.cipher.organizationUseTotp + : this.canAccessPremium; + + if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) { await this.totpUpdateCode(); const interval = this.totpService.getTimeInterval(this.cipher.login.totp); await this.totpTick(interval); diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 8503604bf7c..b4a0d4841f8 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -116,7 +116,7 @@

{{ "loginCredentials" | i18n }}

{{ "verificationCodeTotp" | i18n }} {{ "loginCredentials" | i18n }}

id="totp" readonly bitInput - [type]="!(isPremium$ | async) ? 'password' : 'text'" + [type]="!(allowTotpGeneration$ | async) ? 'password' : 'text'" [value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'" aria-readonly="true" data-testid="login-totp" class="tw-font-mono" />
{{ "loginCredentials" | i18n }} showToast [appA11yTitle]="'copyVerificationCode' | i18n" data-testid="copy-totp" - [disabled]="!(isPremium$ | async)" + [disabled]="!(allowTotpGeneration$ | async)" class="disabled:tw-cursor-default" > diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts index 7533ac26471..281e187f78b 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -2,7 +2,15 @@ // @ts-strict-ignore import { CommonModule, DatePipe } from "@angular/common"; import { Component, inject, Input } from "@angular/core"; -import { Observable, switchMap } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + filter, + map, + Observable, + shareReplay, + switchMap, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -12,13 +20,13 @@ import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { + BadgeModule, + ColorPasswordModule, FormFieldModule, + IconButtonModule, SectionComponent, SectionHeaderComponent, TypographyModule, - IconButtonModule, - BadgeModule, - ColorPasswordModule, } from "@bitwarden/components"; // FIXME: remove `src` and fix import @@ -51,13 +59,31 @@ type TotpCodeValues = { ], }) export class LoginCredentialsViewComponent { - @Input() cipher: CipherView; + @Input() + get cipher(): CipherView { + return this._cipher$.value; + } + set cipher(value: CipherView) { + this._cipher$.next(value); + } + private _cipher$ = new BehaviorSubject(null); - isPremium$: Observable = this.accountService.activeAccount$.pipe( + private _userHasPremium$: Observable = this.accountService.activeAccount$.pipe( switchMap((account) => this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ), ); + + allowTotpGeneration$: Observable = combineLatest([ + this._userHasPremium$, + this._cipher$.pipe(filter((c) => c != null)), + ]).pipe( + map(([userHasPremium, cipher]) => { + // User premium status only applies to personal ciphers, organizationUseTotp applies to organization ciphers + return (userHasPremium && cipher.organizationId == null) || cipher.organizationUseTotp; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); showPasswordCount: boolean = false; passwordRevealed: boolean = false; totpCodeCopyObj: TotpCodeValues;