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 @@
+
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 @@
{{ "verificationCodeTotp" | i18n }}
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;