diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4ba6c6999ac..e730122d1c4 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -582,8 +582,8 @@ "archiveItem": { "message": "Archive item" }, - "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "archiveItemDialogContent": { + "message": "Once archived, this item will be excluded from search results and autofill suggestions." }, "archived": { "message": "Archived" diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index ce797d9755e..f881b07282b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -376,7 +376,8 @@ export class ItemMoreOptionsComponent { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "archiveItem" }, - content: { key: "archiveItemConfirmDesc" }, + content: { key: "archiveItemDialogContent" }, + acceptButtonText: { key: "archiveVerb" }, type: "info", }); diff --git a/apps/browser/src/vault/popup/settings/archive.component.html b/apps/browser/src/vault/popup/settings/archive.component.html index 16afab4384b..01ac799ba29 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.html +++ b/apps/browser/src/vault/popup/settings/archive.component.html @@ -42,9 +42,23 @@ {{ cipher.name }} - @if (CipherViewLikeUtils.hasAttachments(cipher)) { - - } +
+ @if (cipher.organizationId) { + + } + @if (CipherViewLikeUtils.hasAttachments(cipher)) { + + } +
{{ CipherViewLikeUtils.subtitle(cipher) }} diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index ecf091a7322..8a81d733039 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -1,11 +1,13 @@ import { CommonModule } from "@angular/common"; import { Component, inject } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; import { combineLatest, firstValueFrom, map, Observable, startWith, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -33,6 +35,7 @@ import { import { CanDeleteCipherDirective, DecryptionFailureDialogComponent, + OrgIconDirective, PasswordRepromptService, } from "@bitwarden/vault"; @@ -60,6 +63,7 @@ import { ROUTES_AFTER_EDIT_DELETION } from "../components/vault-v2/add-edit/add- SectionComponent, SectionHeaderComponent, TypographyModule, + OrgIconDirective, CardComponent, ButtonComponent, ], @@ -79,6 +83,26 @@ export class ArchiveComponent { private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); + private readonly orgMap = toSignal( + this.userId$.pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe( + map((orgs) => { + const map = new Map(); + for (const org of orgs) { + map.set(org.id, org); + } + return map; + }), + ), + ), + ), + ); + + private readonly collections = toSignal( + this.userId$.pipe(switchMap((userId) => this.collectionService.decryptedCollections$(userId))), + ); + protected archivedCiphers$ = this.userId$.pipe( switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)), ); @@ -242,4 +266,22 @@ export class ArchiveComponent { return this.passwordRepromptService.passwordRepromptCheck(cipher); } + + /** + * Get the organization tier type for the given cipher. + */ + orgTierType({ organizationId }: CipherViewLike) { + return this.orgMap()?.get(organizationId as string)?.productTierType; + } + + /** + * Get the organization icon tooltip for the given cipher. + */ + orgIconTooltip({ collectionIds }: CipherViewLike) { + if (collectionIds.length !== 1) { + return this.i18nService.t("nCollections", collectionIds.length); + } + + return this.collections()?.find((c) => c.id === collectionIds[0])?.name; + } } diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 879c5621105..b75275107eb 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -2949,9 +2949,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index fec4dc41982..facd9554af1 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -58,7 +58,7 @@ security-framework = "=3.5.1" security-framework-sys = "=2.15.0" serde = "=1.0.209" serde_json = "=1.0.127" -sha2 = "=0.10.8" +sha2 = "=0.10.9" ssh-encoding = "=0.2.0" ssh-key = { version = "=0.6.7", default-features = false } sysinfo = "=0.37.2" diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 56eacc94e50..f9947e16692 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4397,8 +4397,8 @@ "archiveItem": { "message": "Archive item" }, - "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "archiveItemDialogContent": { + "message": "Once archived, this item will be excluded from search results and autofill suggestions." }, "unArchiveAndSave": { "message": "Unarchive and save" diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts index e23d215aef1..6a801e78ec2 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts @@ -12,7 +12,7 @@ import { VaultFilter, CollectionFilter } from "@bitwarden/vault"; imports: [A11yTitleDirective, NavigationModule], }) export class CollectionFilterComponent { - protected readonly collection = input>(); + protected readonly collection = input.required>(); protected readonly activeFilter = input(); protected readonly displayName = computed(() => { diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts index 0f24fe7aecf..dd2d5b504c8 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts @@ -13,7 +13,7 @@ import { VaultFilter, FolderFilter } from "@bitwarden/vault"; imports: [A11yTitleDirective, NavigationModule, IconButtonModule, I18nPipe], }) export class FolderFilterComponent { - protected readonly folder = input>(); + protected readonly folder = input.required>(); protected readonly activeFilter = input(); protected onEditFolder = output(); diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts index fa91816577a..520c29833e3 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Component, computed, input, inject } from "@angular/core"; import { DisplayMode } from "@bitwarden/angular/vault/vault-filter/models/display-mode"; @@ -20,7 +22,7 @@ export class OrganizationFilterComponent { private vaultFilterService: VaultFilterServiceAbstraction = inject(VaultFilterServiceAbstraction); protected readonly hide = input(false); - protected readonly organizations = input>(); + protected readonly organizations = input.required>(); protected readonly activeFilter = input(); protected readonly activeOrganizationDataOwnership = input(false); protected readonly activeSingleOrganizationPolicy = input(false); @@ -56,7 +58,6 @@ export class OrganizationFilterComponent { if (!organization.node.enabled) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("disabledOrganizationFilterError"), }); return; diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html index aef9a4d41b4..b6b22a3c68d 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html @@ -6,10 +6,13 @@ [text]="archiveFilter.name | i18n" [attr.aria-pressed]="activeFilter()?.isArchived" [appA11yTitle]="archiveFilter.name | i18n" - /> - @if (!(canArchive$ | async)) { - - } + > + @if (!(canArchive$ | async)) { + + + + } + } (); + protected readonly activeFilter = input.required(); + + private readonly premiumBadgeComponent = viewChild(PremiumBadgeComponent); + protected readonly archiveFilter: CipherTypeFilter = { id: "archive", name: "archiveNoun", @@ -38,7 +43,7 @@ export class StatusFilterComponent { }; protected applyFilter(filterType: CipherStatus) { - let filter: CipherTypeFilter = null; + let filter: CipherTypeFilter | null = null; if (filterType === "archive") { filter = this.archiveFilter; } else if (filterType === "trash") { @@ -50,8 +55,6 @@ export class StatusFilterComponent { } } - private readonly premiumBadgeComponent = viewChild.required(PremiumBadgeComponent); - private userId$ = this.accountService.activeAccount$.pipe(getUserId); protected canArchive$ = this.userId$.pipe( switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)), @@ -71,7 +74,7 @@ export class StatusFilterComponent { if (canArchive || hasArchivedCiphers) { this.applyFilter("archive"); } else { - await this.premiumBadgeComponent().promptForPremium(event); + await this.premiumBadgeComponent()?.promptForPremium(event); } } } diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts index 40755b25253..e5e7a7691e4 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts @@ -20,7 +20,7 @@ export class TypeFilterComponent { RestrictedItemTypesService, ); - protected readonly cipherTypes = input>(); + protected readonly cipherTypes = input.required>(); protected readonly activeFilter = input(); protected applyTypeFilter(event: Event, cipherType: TreeNode) { diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts index aa54c736024..a858e40bf5e 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, inject, OnInit, output, computed, signal } from "@angular/core"; import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.ts index 64f850826a3..c104f76ff2d 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; 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 cad2c97557b..527df0b5370 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -746,7 +746,8 @@ export class VaultComponent implements OnInit, OnDestr const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "archiveItem" }, - content: { key: "archiveItemConfirmDesc" }, + content: { key: "archiveItemDialogContent" }, + acceptButtonText: { key: "archiveVerb" }, type: "info", }); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index de77354efb6..e296e76e983 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -11707,8 +11707,8 @@ "message": "Archive item", "description": "Verb" }, - "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "archiveItemDialogContent": { + "message": "Once archived, this item will be excluded from search results and autofill suggestions." }, "archiveBulkItems": { "message": "Archive items", diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts index 71d228ff822..cbaece1b442 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts @@ -264,6 +264,13 @@ export abstract class OrganizationUserApiService { ids: string[], ): Promise>; + /** + * Revoke the current user's access to the organization + * if they decline an item transfer under the Organization Data Ownership policy. + * @param organizationId - Identifier for the organization the user belongs to + */ + abstract revokeSelf(organizationId: string): Promise; + /** * Restore an organization user's access to the organization * @param organizationId - Identifier for the organization the user belongs to diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts index 869d84a8c8e..536afd2b3f6 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts @@ -339,6 +339,16 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer return new ListResponse(r, OrganizationUserBulkResponse); } + revokeSelf(organizationId: string): Promise { + return this.apiService.send( + "PUT", + "/organizations/" + organizationId + "/users/revoke-self", + null, + true, + false, + ); + } + restoreOrganizationUser(organizationId: string, id: string): Promise { return this.apiService.send( "PUT", diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index bc065155fdb..db8eacb5fa4 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -280,8 +280,7 @@ export abstract class KeyService { * encrypted private key at all. * * @param userId The user id of the user to get the data for. - * @returns An observable stream of the decrypted private key or null. - * @throws Error when decryption of the encrypted private key fails. + * @returns An observable stream of the decrypted private key or null if the private key is not present or fails to decrypt */ abstract userPrivateKey$(userId: UserId): Observable; diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index c0a0ab62347..9d96d7c09b1 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -437,14 +437,13 @@ describe("keyService", () => { ); }); - it("throws an error if unwrapping encrypted private key fails", async () => { + it("emits null if unwrapping encrypted private key fails", async () => { encryptService.unwrapDecapsulationKey.mockImplementationOnce(() => { throw new Error("Unwrapping failed"); }); - await expect(firstValueFrom(keyService.userPrivateKey$(mockUserId))).rejects.toThrow( - "Unwrapping failed", - ); + const result = await firstValueFrom(keyService.userPrivateKey$(mockUserId)); + expect(result).toBeNull(); }); it("returns null if user key is not set", async () => { diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 752a89e5fcd..4c749e9f6c4 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -791,7 +791,10 @@ export class DefaultKeyService implements KeyServiceAbstraction { return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$; } - private userPrivateKeyHelper$(userId: UserId) { + private userPrivateKeyHelper$(userId: UserId): Observable<{ + userKey: UserKey; + userPrivateKey: UserPrivateKey | null; + } | null> { const userKey$ = this.userKey$(userId); return userKey$.pipe( switchMap((userKey) => { @@ -801,18 +804,20 @@ export class DefaultKeyService implements KeyServiceAbstraction { return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$.pipe( switchMap(async (encryptedPrivateKey) => { - try { - return await this.decryptPrivateKey(encryptedPrivateKey, userKey); - } catch (e) { - this.logService.error("Failed to decrypt private key for user ", userId, e); - throw e; - } + return await this.decryptPrivateKey(encryptedPrivateKey, userKey); }), // Combine outerscope info with user private key map((userPrivateKey) => ({ userKey, userPrivateKey, })), + catchError((err: unknown) => { + this.logService.error(`Failed to decrypt private key for user ${userId}`); + return of({ + userKey, + userPrivateKey: null, + }); + }), ); }), ); diff --git a/libs/tools/send/send-ui/src/send-list/send-list.component.ts b/libs/tools/send/send-ui/src/send-list/send-list.component.ts index d90f77913aa..17fc006feef 100644 --- a/libs/tools/send/send-ui/src/send-list/send-list.component.ts +++ b/libs/tools/send/send-ui/src/send-list/send-list.component.ts @@ -1,17 +1,8 @@ import { CommonModule } from "@angular/common"; -import { - ChangeDetectionStrategy, - Component, - computed, - effect, - inject, - input, - output, -} from "@angular/core"; +import { ChangeDetectionStrategy, Component, computed, effect, input, output } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NoResults, NoSendsIcon } from "@bitwarden/assets/svg"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { ButtonModule, @@ -57,8 +48,6 @@ export class SendListComponent { protected readonly noResultsIcon = NoResults; protected readonly sendListState = SendListState; - private i18nService = inject(I18nService); - readonly sends = input.required(); readonly loading = input(false); readonly disableSend = input(false); @@ -70,7 +59,7 @@ export class SendListComponent { ); protected readonly noSearchResults = computed( - () => this.showSearchBar() && (this.sends().length === 0 || this.searchText().length > 0), + () => this.showSearchBar() && this.sends().length === 0, ); // Reusable data source instance - updated reactively when sends change diff --git a/libs/vault/src/services/archive-cipher-utilities.service.ts b/libs/vault/src/services/archive-cipher-utilities.service.ts index 5d3c5c33236..93e752b57dd 100644 --- a/libs/vault/src/services/archive-cipher-utilities.service.ts +++ b/libs/vault/src/services/archive-cipher-utilities.service.ts @@ -41,7 +41,8 @@ export class ArchiveCipherUtilitiesService { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "archiveItem" }, - content: { key: "archiveItemConfirmDesc" }, + content: { key: "archiveItemDialogContent" }, + acceptButtonText: { key: "archiveVerb" }, type: "info", }); diff --git a/libs/vault/src/services/default-vault-items-transfer.service.spec.ts b/libs/vault/src/services/default-vault-items-transfer.service.spec.ts index 4ad9c53c6f0..51154c3cee9 100644 --- a/libs/vault/src/services/default-vault-items-transfer.service.spec.ts +++ b/libs/vault/src/services/default-vault-items-transfer.service.spec.ts @@ -2,7 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of, Subject } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { CollectionService } from "@bitwarden/admin-console/common"; +import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -42,6 +42,7 @@ describe("DefaultVaultItemsTransferService", () => { let mockToastService: MockProxy; let mockEventCollectionService: MockProxy; let mockConfigService: MockProxy; + let mockOrganizationUserApiService: MockProxy; const userId = "user-id" as UserId; const organizationId = "org-id" as OrganizationId; @@ -77,6 +78,7 @@ describe("DefaultVaultItemsTransferService", () => { mockToastService = mock(); mockEventCollectionService = mock(); mockConfigService = mock(); + mockOrganizationUserApiService = mock(); mockI18nService.t.mockImplementation((key) => key); transferInProgressValues = []; @@ -92,6 +94,7 @@ describe("DefaultVaultItemsTransferService", () => { mockToastService, mockEventCollectionService, mockConfigService, + mockOrganizationUserApiService, ); }); @@ -632,9 +635,15 @@ describe("DefaultVaultItemsTransferService", () => { mockDialogService.open .mockReturnValueOnce(createMockDialogRef(TransferItemsDialogResult.Declined)) .mockReturnValueOnce(createMockDialogRef(LeaveConfirmationDialogResult.Confirmed)); + mockOrganizationUserApiService.revokeSelf.mockResolvedValue(undefined); await service.enforceOrganizationDataOwnership(userId); + expect(mockOrganizationUserApiService.revokeSelf).toHaveBeenCalledWith(organizationId); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + message: "leftOrganization", + }); expect(mockCipherService.shareManyWithServer).not.toHaveBeenCalled(); }); diff --git a/libs/vault/src/services/default-vault-items-transfer.service.ts b/libs/vault/src/services/default-vault-items-transfer.service.ts index c184b2c902e..6009fc97e7c 100644 --- a/libs/vault/src/services/default-vault-items-transfer.service.ts +++ b/libs/vault/src/services/default-vault-items-transfer.service.ts @@ -10,7 +10,7 @@ import { } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { CollectionService } from "@bitwarden/admin-console/common"; +import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -53,6 +53,7 @@ export class DefaultVaultItemsTransferService implements VaultItemsTransferServi private toastService: ToastService, private eventCollectionService: EventCollectionService, private configService: ConfigService, + private organizationUserApiService: OrganizationUserApiService, ) {} private _transferInProgressSubject = new BehaviorSubject(false); @@ -162,7 +163,12 @@ export class DefaultVaultItemsTransferService implements VaultItemsTransferServi ); if (!userAcceptedTransfer) { - // TODO: Revoke user from organization if they decline migration and show toast PM-29465 + await this.organizationUserApiService.revokeSelf(migrationInfo.enforcingOrganization.id); + + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("leftOrganization"), + }); await this.eventCollectionService.collect( EventType.Organization_ItemOrganization_Declined,