mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 09:13:33 +00:00
Merge branch 'main' into autofill/pm-5189-fix-issues-present-with-inline-menu-rendering-in-iframes
This commit is contained in:
12
.github/workflows/build-browser.yml
vendored
12
.github/workflows/build-browser.yml
vendored
@@ -189,12 +189,12 @@ jobs:
|
||||
path: browser-source/apps/browser/dist/dist-chrome.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# - name: Upload Chrome MV3 artifact
|
||||
# uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
# with:
|
||||
# name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
|
||||
# path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
|
||||
# if-no-files-found: error
|
||||
- name: Upload Chrome MV3 artifact (DO NOT USE FOR PROD)
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
|
||||
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Firefox artifact
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
|
||||
@@ -18,11 +18,13 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
||||
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
|
||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
@@ -232,6 +234,7 @@ export class Main {
|
||||
stateEventRunnerService: StateEventRunnerService;
|
||||
biometricStateService: BiometricStateService;
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||
providerApiService: ProviderApiServiceAbstraction;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@@ -692,6 +695,8 @@ export class Main {
|
||||
this.eventUploadService,
|
||||
this.authService,
|
||||
);
|
||||
|
||||
this.providerApiService = new ProviderApiService(this.apiService);
|
||||
}
|
||||
|
||||
async run() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.4.0",
|
||||
"version": "2024.4.1",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
</bit-tab>
|
||||
|
||||
<bit-tab label="{{ 'members' | i18n }}">
|
||||
<p>{{ "editGroupMembersDesc" | i18n }}</p>
|
||||
<p>
|
||||
{{ "editGroupMembersDesc" | i18n }}
|
||||
<span *ngIf="restrictGroupAccess$ | async">
|
||||
{{ "restrictedGroupAccessDesc" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
<bit-access-selector
|
||||
formControlName="members"
|
||||
[items]="members"
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { catchError, combineLatest, from, map, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||
@@ -88,10 +104,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
tabIndex: GroupAddEditTabType;
|
||||
loading = true;
|
||||
editMode = false;
|
||||
title: string;
|
||||
collections: AccessItemView[] = [];
|
||||
members: AccessItemView[] = [];
|
||||
members: Array<AccessItemView & { userId: UserId }> = [];
|
||||
group: GroupView;
|
||||
|
||||
groupForm = this.formBuilder.group({
|
||||
@@ -110,6 +125,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
return this.params.organizationId;
|
||||
}
|
||||
|
||||
protected get editMode(): boolean {
|
||||
return this.groupId != null;
|
||||
}
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private get orgCollections$() {
|
||||
@@ -134,7 +153,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
private get orgMembers$() {
|
||||
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
|
||||
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
|
||||
map((response) =>
|
||||
response.data.map((m) => ({
|
||||
@@ -145,13 +164,15 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email,
|
||||
labelName: m.name || m.email,
|
||||
status: m.status,
|
||||
userId: m.userId as UserId,
|
||||
})),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private get groupDetails$() {
|
||||
if (!this.editMode) {
|
||||
private groupDetails$: Observable<GroupView | undefined> = of(this.editMode).pipe(
|
||||
concatMap((editMode) => {
|
||||
if (!editMode) {
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
@@ -172,7 +193,26 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
return of(undefined);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}),
|
||||
shareReplay({ refCount: false }),
|
||||
);
|
||||
|
||||
restrictGroupAccess$ = combineLatest([
|
||||
this.organizationService.get$(this.organizationId),
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||
this.groupDetails$,
|
||||
]).pipe(
|
||||
map(
|
||||
([organization, flexibleCollectionsV1Enabled, group]) =>
|
||||
// Feature flag conditionals
|
||||
flexibleCollectionsV1Enabled &&
|
||||
organization.flexibleCollections &&
|
||||
// Business logic conditionals
|
||||
!organization.allowAdminAccessToAllCollectionItems &&
|
||||
group !== undefined,
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||
@@ -188,17 +228,25 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dialogService: DialogService,
|
||||
private organizationService: OrganizationService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.editMode = this.loading = this.groupId != null;
|
||||
this.loading = true;
|
||||
this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup");
|
||||
|
||||
combineLatest([this.orgCollections$, this.orgMembers$, this.groupDetails$])
|
||||
combineLatest([
|
||||
this.orgCollections$,
|
||||
this.orgMembers$,
|
||||
this.groupDetails$,
|
||||
this.restrictGroupAccess$,
|
||||
this.accountService.activeAccount$,
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([collections, members, group]) => {
|
||||
.subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => {
|
||||
this.collections = collections;
|
||||
this.members = members;
|
||||
this.group = group;
|
||||
@@ -224,6 +272,18 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
// If the current user is not already in the group and cannot add themselves, remove them from the list
|
||||
if (restrictGroupAccess) {
|
||||
const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id;
|
||||
const isAlreadyInGroup = this.groupForm.value.members.some(
|
||||
(m) => m.id === organizationUserId,
|
||||
);
|
||||
|
||||
if (!isAlreadyInGroup) {
|
||||
this.members = this.members.filter((m) => m.id !== organizationUserId);
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
GroupService,
|
||||
@@ -206,25 +207,52 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
collections: this.collectionAdminService.getAll(this.params.organizationId),
|
||||
userDetails: userDetails$,
|
||||
groups: groups$,
|
||||
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
),
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(({ organization, collections, userDetails, groups }) => {
|
||||
.subscribe(
|
||||
({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => {
|
||||
this.setFormValidators(organization);
|
||||
|
||||
this.collectionAccessItems = [].concat(
|
||||
collections.map((c) => mapCollectionToAccessItemView(c)),
|
||||
);
|
||||
|
||||
// Groups tab: populate available groups
|
||||
this.groupAccessItems = [].concat(
|
||||
groups.map<AccessItemView>((g) => mapGroupToAccessItemView(g)),
|
||||
);
|
||||
|
||||
if (this.params.organizationUserId) {
|
||||
this.loadOrganizationUser(userDetails, groups, collections);
|
||||
// Collections tab: Populate all available collections (including current user access where applicable)
|
||||
this.collectionAccessItems = collections
|
||||
.map((c) =>
|
||||
mapCollectionToAccessItemView(
|
||||
c,
|
||||
organization,
|
||||
flexibleCollectionsV1Enabled,
|
||||
userDetails == null
|
||||
? undefined
|
||||
: c.users.find((access) => access.id === userDetails.id),
|
||||
),
|
||||
)
|
||||
// But remove collections that we can't assign access to, unless the user is already assigned
|
||||
.filter(
|
||||
(item) =>
|
||||
!item.readonly || userDetails?.collections.some((access) => access.id == item.id),
|
||||
);
|
||||
|
||||
if (userDetails != null) {
|
||||
this.loadOrganizationUser(
|
||||
userDetails,
|
||||
groups,
|
||||
collections,
|
||||
organization,
|
||||
flexibleCollectionsV1Enabled,
|
||||
);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private setFormValidators(organization: Organization) {
|
||||
@@ -246,7 +274,9 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
private loadOrganizationUser(
|
||||
userDetails: OrganizationUserAdminView,
|
||||
groups: GroupView[],
|
||||
collections: CollectionView[],
|
||||
collections: CollectionAdminView[],
|
||||
organization: Organization,
|
||||
flexibleCollectionsV1Enabled: boolean,
|
||||
) {
|
||||
if (!userDetails) {
|
||||
throw new Error("Could not find user to edit.");
|
||||
@@ -295,13 +325,22 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
}),
|
||||
);
|
||||
|
||||
// Populate additional collection access via groups (rendered as separate rows from user access)
|
||||
this.collectionAccessItems = this.collectionAccessItems.concat(
|
||||
collectionsFromGroups.map(({ collection, accessSelection, group }) =>
|
||||
mapCollectionToAccessItemView(collection, accessSelection, group),
|
||||
mapCollectionToAccessItemView(
|
||||
collection,
|
||||
organization,
|
||||
flexibleCollectionsV1Enabled,
|
||||
accessSelection,
|
||||
group,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const accessSelections = mapToAccessSelections(userDetails);
|
||||
// Set current collections and groups the user has access to (excluding collections the current user doesn't have
|
||||
// permissions to change - they are included as readonly via the CollectionAccessItems)
|
||||
const accessSelections = mapToAccessSelections(userDetails, this.collectionAccessItems);
|
||||
const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups);
|
||||
|
||||
this.formGroup.removeControl("emails");
|
||||
@@ -573,6 +612,8 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
|
||||
function mapCollectionToAccessItemView(
|
||||
collection: CollectionView,
|
||||
organization: Organization,
|
||||
flexibleCollectionsV1Enabled: boolean,
|
||||
accessSelection?: CollectionAccessSelectionView,
|
||||
group?: GroupView,
|
||||
): AccessItemView {
|
||||
@@ -581,7 +622,8 @@ function mapCollectionToAccessItemView(
|
||||
id: group ? `${collection.id}-${group.id}` : collection.id,
|
||||
labelName: collection.name,
|
||||
listName: collection.name,
|
||||
readonly: group !== undefined,
|
||||
readonly:
|
||||
group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled),
|
||||
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
|
||||
viaGroupName: group?.name,
|
||||
};
|
||||
@@ -596,16 +638,23 @@ function mapGroupToAccessItemView(group: GroupView): AccessItemView {
|
||||
};
|
||||
}
|
||||
|
||||
function mapToAccessSelections(user: OrganizationUserAdminView): AccessItemValue[] {
|
||||
function mapToAccessSelections(
|
||||
user: OrganizationUserAdminView,
|
||||
items: AccessItemView[],
|
||||
): AccessItemValue[] {
|
||||
if (user == undefined) {
|
||||
return [];
|
||||
}
|
||||
return [].concat(
|
||||
user.collections.map<AccessItemValue>((selection) => ({
|
||||
|
||||
return (
|
||||
user.collections
|
||||
// The FormControl value only represents editable collection access - exclude readonly access selections
|
||||
.filter((selection) => !items.find((item) => item.id == selection.id).readonly)
|
||||
.map<AccessItemValue>((selection) => ({
|
||||
id: selection.id,
|
||||
type: AccessItemType.Collection,
|
||||
permission: convertToPermission(selection),
|
||||
})),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "deleteProvider" | i18n }}</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-callout type="warning">{{ "deleteProviderWarning" | i18n }}</app-callout>
|
||||
<p class="text-center">
|
||||
<strong>{{ name }}</strong>
|
||||
</p>
|
||||
<p>{{ "deleteProviderRecoverConfirmDesc" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-danger btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span>{{ "deleteProvider" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-verify-recover-delete.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-verify-recover-delete-provider",
|
||||
templateUrl: "verify-recover-delete-provider.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class VerifyRecoverDeleteProviderComponent implements OnInit {
|
||||
name: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private providerId: string;
|
||||
private token: string;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const qParams = await firstValueFrom(this.route.queryParams);
|
||||
if (qParams.providerId != null && qParams.token != null && qParams.name != null) {
|
||||
this.providerId = qParams.providerId;
|
||||
this.token = qParams.token;
|
||||
this.name = qParams.name;
|
||||
} else {
|
||||
await this.router.navigate(["/"]);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new ProviderVerifyRecoverDeleteRequest(this.token);
|
||||
this.formPromise = this.providerApiService.providerRecoverDeleteToken(
|
||||
this.providerId,
|
||||
request,
|
||||
);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("providerDeleted"),
|
||||
this.i18nService.t("providerDeletedDesc"),
|
||||
);
|
||||
await this.router.navigate(["/"]);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
<div class="d-flex tabbed-header">
|
||||
<h1>
|
||||
<div class="tw-flex tw-justify-between tw-mb-2 tw-pb-2 tw-mt-6">
|
||||
<h2 bitTypography="h2">
|
||||
{{ "billingHistory" | i18n }}
|
||||
</h1>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="load()"
|
||||
class="tw-ml-auto"
|
||||
*ngIf="firstLoaded"
|
||||
[disabled]="loading"
|
||||
>
|
||||
@@ -17,11 +16,11 @@
|
||||
</div>
|
||||
<ng-container *ngIf="!firstLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="billing">
|
||||
<app-billing-history [billing]="billing"></app-billing-history>
|
||||
|
||||
@@ -170,8 +170,8 @@
|
||||
</div>
|
||||
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustStorage">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="adjustStorage(true)">
|
||||
<div class="d-flex">
|
||||
<button bitButton type="button" buttonType="secondary" [bitAction]="adjustStorage(true)">
|
||||
{{ "addStorage" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
@@ -179,18 +179,11 @@
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
class="tw-ml-1"
|
||||
(click)="adjustStorage(false)"
|
||||
[bitAction]="adjustStorage(false)"
|
||||
>
|
||||
{{ "removeStorage" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-storage
|
||||
[storageGbPrice]="4"
|
||||
[add]="adjustStorageAdd"
|
||||
(onAdjusted)="closeStorage(true)"
|
||||
(onCanceled)="closeStorage(false)"
|
||||
*ngIf="showAdjustStorage"
|
||||
></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -12,6 +12,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
AdjustStorageDialogResult,
|
||||
openAdjustStorageDialog,
|
||||
} from "../shared/adjust-storage.component";
|
||||
import {
|
||||
OffboardingSurveyDialogResultType,
|
||||
openOffboardingSurvey,
|
||||
@@ -24,7 +28,6 @@ export class UserSubscriptionComponent implements OnInit {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showUpdateLicense = false;
|
||||
sub: SubscriptionResponse;
|
||||
selfHosted = false;
|
||||
@@ -144,19 +147,20 @@ export class UserSubscriptionComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
adjustStorage(add: boolean) {
|
||||
this.adjustStorageAdd = add;
|
||||
this.showAdjustStorage = true;
|
||||
}
|
||||
|
||||
closeStorage(load: boolean) {
|
||||
this.showAdjustStorage = false;
|
||||
if (load) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
}
|
||||
adjustStorage = (add: boolean) => {
|
||||
return async () => {
|
||||
const dialogRef = openAdjustStorageDialog(this.dialogService, {
|
||||
data: {
|
||||
storageGbPrice: 4,
|
||||
add: add,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AdjustStorageDialogResult.Adjusted) {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
get subscriptionMarkedForCancel() {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<div class="card card-org-plans">
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h2 class="card-body-header">{{ "changeBillingPlan" | i18n }}</h2>
|
||||
<p class="mb-0">{{ "changeBillingPlanUpgrade" | i18n }}</p>
|
||||
<div
|
||||
class="tw-relative tw-flex tw-flex-col tw-min-w-0 tw-rounded tw-border tw-border-solid tw-border-secondary-300"
|
||||
>
|
||||
<div class="tw-flex-auto tw-p-5">
|
||||
<button
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="main"
|
||||
type="button"
|
||||
size="small"
|
||||
class="tw-float-right"
|
||||
appA11yTitle="{{ 'cancel' | i18n }}"
|
||||
(click)="cancel()"
|
||||
></button>
|
||||
<h2 bitTypography="h2">{{ "changeBillingPlan" | i18n }}</h2>
|
||||
<p bitTypography="body1" class="tw-mb-0">{{ "changeBillingPlanUpgrade" | i18n }}</p>
|
||||
<app-organization-plans
|
||||
[showFree]="false"
|
||||
[showCancel]="true"
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
<bit-container>
|
||||
<ng-container *ngIf="!firstLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="billing">
|
||||
<app-billing-history [billing]="billing"></app-billing-history>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request";
|
||||
@@ -147,6 +148,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
private messagingService: MessagingService,
|
||||
private formBuilder: FormBuilder,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
@@ -182,7 +184,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
if (this.hasProvider) {
|
||||
this.formGroup.controls.businessOwned.setValue(true);
|
||||
this.changedOwnedBusiness();
|
||||
this.provider = await this.apiService.getProvider(this.providerId);
|
||||
this.provider = await this.providerApiService.getProvider(this.providerId);
|
||||
const providerDefaultPlan = this.passwordManagerPlans.find(
|
||||
(plan) => plan.type === PlanType.TeamsAnnually,
|
||||
);
|
||||
|
||||
@@ -175,23 +175,24 @@
|
||||
<bit-progress [barWidth]="storagePercentage" bgColor="success"></bit-progress>
|
||||
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<div class="tw-mt-3">
|
||||
<div class="tw-flex tw-space-x-2" *ngIf="!showAdjustStorage">
|
||||
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(true)">
|
||||
<div class="tw-flex tw-space-x-2">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
type="button"
|
||||
[bitAction]="adjustStorage(true)"
|
||||
>
|
||||
{{ "addStorage" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(false)">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
type="button"
|
||||
[bitAction]="adjustStorage(false)"
|
||||
>
|
||||
{{ "removeStorage" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-storage
|
||||
[storageGbPrice]="storageGbPrice"
|
||||
[add]="adjustStorageAdd"
|
||||
[organizationId]="organizationId"
|
||||
[interval]="billingInterval"
|
||||
(onAdjusted)="closeStorage(true)"
|
||||
(onCanceled)="closeStorage(false)"
|
||||
*ngIf="showAdjustStorage"
|
||||
></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showAdjustSecretsManager">
|
||||
|
||||
@@ -18,6 +18,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
AdjustStorageDialogResult,
|
||||
openAdjustStorageDialog,
|
||||
} from "../shared/adjust-storage.component";
|
||||
import {
|
||||
OffboardingSurveyDialogResultType,
|
||||
openOffboardingSurvey,
|
||||
@@ -36,8 +40,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
userOrg: Organization;
|
||||
showChangePlan = false;
|
||||
showDownloadLicense = false;
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
hasBillingSyncToken: boolean;
|
||||
showAdjustSecretsManager = false;
|
||||
showSecretsManagerSubscribe = false;
|
||||
@@ -361,19 +363,22 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
this.load();
|
||||
}
|
||||
|
||||
adjustStorage(add: boolean) {
|
||||
this.adjustStorageAdd = add;
|
||||
this.showAdjustStorage = true;
|
||||
}
|
||||
|
||||
closeStorage(load: boolean) {
|
||||
this.showAdjustStorage = false;
|
||||
if (load) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
}
|
||||
adjustStorage = (add: boolean) => {
|
||||
return async () => {
|
||||
const dialogRef = openAdjustStorageDialog(this.dialogService, {
|
||||
data: {
|
||||
storageGbPrice: this.storageGbPrice,
|
||||
add: add,
|
||||
organizationId: this.organizationId,
|
||||
interval: this.billingInterval,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AdjustStorageDialogResult.Adjusted) {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
removeSponsorship = async () => {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog
|
||||
dialogSize="large"
|
||||
[title]="(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n"
|
||||
>
|
||||
<ng-container bitDialogContent>
|
||||
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="DialogResult.Cancelled"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,110 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, ViewChild } from "@angular/core";
|
||||
import { FormGroup } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent } from "./payment.component";
|
||||
import { TaxInfoComponent } from "./tax-info.component";
|
||||
|
||||
export interface AdjustPaymentDialogData {
|
||||
organizationId: string;
|
||||
currentType: PaymentMethodType;
|
||||
}
|
||||
|
||||
export enum AdjustPaymentDialogResult {
|
||||
Adjusted = "adjusted",
|
||||
Cancelled = "cancelled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "adjust-payment-dialog.component.html",
|
||||
})
|
||||
export class AdjustPaymentDialogComponent {
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
organizationId: string;
|
||||
currentType: PaymentMethodType;
|
||||
paymentMethodType = PaymentMethodType;
|
||||
|
||||
protected DialogResult = AdjustPaymentDialogResult;
|
||||
protected formGroup = new FormGroup({});
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
this.currentType = data.currentType;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const request = new PaymentRequest();
|
||||
const response = this.paymentComponent.createPaymentToken().then((result) => {
|
||||
request.paymentToken = result[0];
|
||||
request.paymentMethodType = result[1];
|
||||
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
|
||||
request.country = this.taxInfoComponent.taxInfo.country;
|
||||
if (this.organizationId == null) {
|
||||
return this.apiService.postAccountPayment(request);
|
||||
} else {
|
||||
request.taxId = this.taxInfoComponent.taxInfo.taxId;
|
||||
request.state = this.taxInfoComponent.taxInfo.state;
|
||||
request.line1 = this.taxInfoComponent.taxInfo.line1;
|
||||
request.line2 = this.taxInfoComponent.taxInfo.line2;
|
||||
request.city = this.taxInfoComponent.taxInfo.city;
|
||||
request.state = this.taxInfoComponent.taxInfo.state;
|
||||
return this.organizationApiService.updatePayment(this.organizationId, request);
|
||||
}
|
||||
});
|
||||
await response;
|
||||
if (this.organizationId) {
|
||||
await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
|
||||
}
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("updatedPaymentMethod"),
|
||||
);
|
||||
this.dialogRef.close(AdjustPaymentDialogResult.Adjusted);
|
||||
};
|
||||
|
||||
changeCountry() {
|
||||
if (this.taxInfoComponent.taxInfo.country === "US") {
|
||||
this.paymentComponent.hideBank = !this.organizationId;
|
||||
} else {
|
||||
this.paymentComponent.hideBank = true;
|
||||
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a AdjustPaymentDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export function openAdjustPaymentDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AdjustPaymentDialogData>,
|
||||
) {
|
||||
return dialogService.open<AdjustPaymentDialogResult>(AdjustPaymentDialogComponent, config);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="card-body-header">
|
||||
{{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
|
||||
</h3>
|
||||
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { PaymentComponent } from "./payment.component";
|
||||
import { TaxInfoComponent } from "./tax-info.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-adjust-payment",
|
||||
templateUrl: "adjust-payment.component.html",
|
||||
})
|
||||
export class AdjustPaymentComponent {
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
@Input() currentType?: PaymentMethodType;
|
||||
@Input() organizationId: string;
|
||||
@Output() onAdjusted = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
paymentMethodType = PaymentMethodType;
|
||||
formPromise: Promise<void>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new PaymentRequest();
|
||||
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
|
||||
request.paymentToken = result[0];
|
||||
request.paymentMethodType = result[1];
|
||||
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
|
||||
request.country = this.taxInfoComponent.taxInfo.country;
|
||||
if (this.organizationId == null) {
|
||||
return this.apiService.postAccountPayment(request);
|
||||
} else {
|
||||
request.taxId = this.taxInfoComponent.taxInfo.taxId;
|
||||
request.state = this.taxInfoComponent.taxInfo.state;
|
||||
request.line1 = this.taxInfoComponent.taxInfo.line1;
|
||||
request.line2 = this.taxInfoComponent.taxInfo.line2;
|
||||
request.city = this.taxInfoComponent.taxInfo.city;
|
||||
request.state = this.taxInfoComponent.taxInfo.state;
|
||||
return this.organizationApiService.updatePayment(this.organizationId, request);
|
||||
}
|
||||
});
|
||||
await this.formPromise;
|
||||
if (this.organizationId) {
|
||||
await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
|
||||
}
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("updatedPaymentMethod"),
|
||||
);
|
||||
this.onAdjusted.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
changeCountry() {
|
||||
if (this.taxInfoComponent.taxInfo.country === "US") {
|
||||
this.paymentComponent.hideBank = !this.organizationId;
|
||||
} else {
|
||||
this.paymentComponent.hideBank = true;
|
||||
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,35 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="card-body-header">{{ (add ? "addStorage" : "removeStorage") | i18n }}</h3>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="storageAdjustment">{{
|
||||
(add ? "gbStorageAdd" : "gbStorageRemove") | i18n
|
||||
}}</label>
|
||||
<input
|
||||
id="storageAdjustment"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="StorageGbAdjustment"
|
||||
[(ngModel)]="storageAdjustment"
|
||||
min="0"
|
||||
max="99"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="add" class="mb-3">
|
||||
<strong>{{ "total" | i18n }}:</strong> {{ storageAdjustment || 0 }} GB ×
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="(add ? 'addStorage' : 'removeStorage') | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}</p>
|
||||
<div class="tw-grid tw-grid-cols-12">
|
||||
<bit-form-field class="tw-col-span-7">
|
||||
<bit-label>{{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="storageAdjustment" />
|
||||
<bit-hint *ngIf="add">
|
||||
<strong>{{ "total" | i18n }}:</strong>
|
||||
{{ formGroup.get("storageAdjustment").value || 0 }} GB ×
|
||||
{{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{
|
||||
interval | i18n
|
||||
}}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="DialogResult.Cancelled"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<small class="d-block text-muted mt-3">
|
||||
{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<app-payment [showMethods]="false"></app-payment>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, ViewChild } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -8,27 +10,45 @@ import { StorageRequest } from "@bitwarden/common/models/request/storage.request
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent } from "./payment.component";
|
||||
|
||||
export interface AdjustStorageDialogData {
|
||||
storageGbPrice: number;
|
||||
add: boolean;
|
||||
organizationId?: string;
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
export enum AdjustStorageDialogResult {
|
||||
Adjusted = "adjusted",
|
||||
Cancelled = "cancelled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-adjust-storage",
|
||||
templateUrl: "adjust-storage.component.html",
|
||||
})
|
||||
export class AdjustStorageComponent {
|
||||
@Input() storageGbPrice = 0;
|
||||
@Input() add = true;
|
||||
@Input() organizationId: string;
|
||||
@Input() interval = "year";
|
||||
@Output() onAdjusted = new EventEmitter<number>();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
storageGbPrice: number;
|
||||
add: boolean;
|
||||
organizationId: string;
|
||||
interval: string;
|
||||
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
|
||||
storageAdjustment = 0;
|
||||
formPromise: Promise<PaymentResponse | void>;
|
||||
protected DialogResult = AdjustStorageDialogResult;
|
||||
protected formGroup = new FormGroup({
|
||||
storageAdjustment: new FormControl(0, [
|
||||
Validators.required,
|
||||
Validators.min(0),
|
||||
Validators.max(99),
|
||||
]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) protected data: AdjustStorageDialogData,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -36,12 +56,16 @@ export class AdjustStorageComponent {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
) {}
|
||||
) {
|
||||
this.storageGbPrice = data.storageGbPrice;
|
||||
this.add = data.add;
|
||||
this.organizationId = data.organizationId;
|
||||
this.interval = data.interval || "year";
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
submit = async () => {
|
||||
const request = new StorageRequest();
|
||||
request.storageGbAdjustment = this.storageAdjustment;
|
||||
request.storageGbAdjustment = this.formGroup.value.storageAdjustment;
|
||||
if (!this.add) {
|
||||
request.storageGbAdjustment *= -1;
|
||||
}
|
||||
@@ -50,12 +74,9 @@ export class AdjustStorageComponent {
|
||||
const action = async () => {
|
||||
let response: Promise<PaymentResponse>;
|
||||
if (this.organizationId == null) {
|
||||
response = this.formPromise = this.apiService.postAccountStorage(request);
|
||||
response = this.apiService.postAccountStorage(request);
|
||||
} else {
|
||||
response = this.formPromise = this.organizationApiService.updateStorage(
|
||||
this.organizationId,
|
||||
request,
|
||||
);
|
||||
response = this.organizationApiService.updateStorage(this.organizationId, request);
|
||||
}
|
||||
const result = await response;
|
||||
if (result != null && result.paymentIntentClientSecret != null) {
|
||||
@@ -69,9 +90,8 @@ export class AdjustStorageComponent {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.formPromise = action();
|
||||
await this.formPromise;
|
||||
this.onAdjusted.emit(this.storageAdjustment);
|
||||
await action();
|
||||
this.dialogRef.close(AdjustStorageDialogResult.Adjusted);
|
||||
if (paymentFailed) {
|
||||
this.platformUtilsService.showToast(
|
||||
"warning",
|
||||
@@ -89,16 +109,21 @@ export class AdjustStorageComponent {
|
||||
this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
};
|
||||
|
||||
get adjustedStorageTotal(): number {
|
||||
return this.storageGbPrice * this.storageAdjustment;
|
||||
return this.storageGbPrice * this.formGroup.value.storageAdjustment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open an AdjustStorageDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export function openAdjustStorageDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AdjustStorageDialogData>,
|
||||
) {
|
||||
return dialogService.open<AdjustStorageDialogResult>(AdjustStorageComponent, config);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<h2 class="mt-3">{{ "invoices" | i18n }}</h2>
|
||||
<p *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p>
|
||||
<table class="table mb-2" *ngIf="invoices && invoices.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let i of invoices">
|
||||
<td>{{ i.date | date: "mediumDate" }}</td>
|
||||
<td>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h3">{{ "invoices" | i18n }}</h3>
|
||||
<p bitTypography="body1" *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p>
|
||||
<bit-table>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let i of invoices">
|
||||
<td bitCell>{{ i.date | date: "mediumDate" }}</td>
|
||||
<td bitCell>
|
||||
<a
|
||||
href="{{ i.pdfUrl }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="mr-2"
|
||||
class="tw-mr-2"
|
||||
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
||||
@@ -18,33 +19,37 @@
|
||||
{{ "invoiceNumber" | i18n: i.number }}</a
|
||||
>
|
||||
</td>
|
||||
<td>{{ i.amount | currency: "$" }}</td>
|
||||
<td>
|
||||
<td bitCell>{{ i.amount | currency: "$" }}</td>
|
||||
<td bitCell>
|
||||
<span *ngIf="i.paid">
|
||||
<i class="bwi bwi-check text-success" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-check tw-text-success" aria-hidden="true"></i>
|
||||
{{ "paid" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="!i.paid">
|
||||
<i class="bwi bwi-exclamation-circle text-muted" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-exclamation-circle tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "unpaid" | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 class="spaced-header">{{ "transactions" | i18n }}</h2>
|
||||
<p *ngIf="!transactions || !transactions.length">{{ "noTransactions" | i18n }}</p>
|
||||
<table class="table mb-2" *ngIf="transactions && transactions.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let t of transactions">
|
||||
<td>{{ t.createdDate | date: "mediumDate" }}</td>
|
||||
<td>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h3">{{ "transactions" | i18n }}</h3>
|
||||
<p bitTypography="body1" *ngIf="!transactions || !transactions.length">
|
||||
{{ "noTransactions" | i18n }}
|
||||
</p>
|
||||
<bit-table *ngIf="transactions && transactions.length">
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let t of transactions">
|
||||
<td bitCell>{{ t.createdDate | date: "mediumDate" }}</td>
|
||||
<td bitCell>
|
||||
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
|
||||
{{ "chargeNoun" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<td bitCell>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
*ngIf="t.paymentMethodType"
|
||||
@@ -56,10 +61,12 @@
|
||||
<td
|
||||
[ngClass]="{ 'text-strike': t.refunded }"
|
||||
title="{{ (t.refunded ? 'refunded' : '') | i18n }}"
|
||||
bitCell
|
||||
>
|
||||
{{ t.amount | currency: "$" }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<small class="text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<small class="tw-text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small>
|
||||
</bit-section>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { AddCreditComponent } from "./add-credit.component";
|
||||
import { AdjustPaymentComponent } from "./adjust-payment.component";
|
||||
import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component";
|
||||
import { AdjustStorageComponent } from "./adjust-storage.component";
|
||||
import { BillingHistoryComponent } from "./billing-history.component";
|
||||
import { OffboardingSurveyComponent } from "./offboarding-survey.component";
|
||||
@@ -18,7 +18,7 @@ import { UpdateLicenseComponent } from "./update-license.component";
|
||||
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
|
||||
declarations: [
|
||||
AddCreditComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustStorageComponent,
|
||||
BillingHistoryComponent,
|
||||
PaymentMethodComponent,
|
||||
|
||||
@@ -102,23 +102,9 @@
|
||||
{{ paymentSource.description }}
|
||||
</p>
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="changePayment()"
|
||||
*ngIf="!showAdjustPayment"
|
||||
>
|
||||
<button type="button" bitButton buttonType="secondary" [bitAction]="changePayment">
|
||||
{{ (paymentSource ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
|
||||
</button>
|
||||
<app-adjust-payment
|
||||
[organizationId]="organizationId"
|
||||
[currentType]="paymentSource != null ? paymentSource.type : null"
|
||||
(onAdjusted)="closePayment(true)"
|
||||
(onCanceled)="closePayment(false)"
|
||||
*ngIf="showAdjustPayment"
|
||||
>
|
||||
</app-adjust-payment>
|
||||
<p *ngIf="isUnpaid">{{ "paymentChargedWithUnpaidSubscription" | i18n }}</p>
|
||||
<ng-container *ngIf="forOrganization">
|
||||
<h2 class="spaced-header">{{ "taxInformation" | i18n }}</h2>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
@@ -14,6 +15,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
AdjustPaymentDialogResult,
|
||||
openAdjustPaymentDialog,
|
||||
} from "./adjust-payment-dialog.component";
|
||||
import { TaxInfoComponent } from "./tax-info.component";
|
||||
|
||||
@Component({
|
||||
@@ -25,7 +30,6 @@ export class PaymentMethodComponent implements OnInit {
|
||||
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
showAdjustPayment = false;
|
||||
showAddCredit = false;
|
||||
billing: BillingPaymentResponse;
|
||||
org: OrganizationSubscriptionResponse;
|
||||
@@ -120,18 +124,18 @@ export class PaymentMethodComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
changePayment() {
|
||||
this.showAdjustPayment = true;
|
||||
}
|
||||
|
||||
closePayment(load: boolean) {
|
||||
this.showAdjustPayment = false;
|
||||
if (load) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
}
|
||||
changePayment = async () => {
|
||||
const dialogRef = openAdjustPaymentDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
currentType: this.paymentSource !== null ? this.paymentSource.type : null,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AdjustPaymentDialogResult.Adjusted) {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
|
||||
async verifyBank() {
|
||||
if (this.loading || !this.forOrganization) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { flagEnabled, Flags } from "../utils/flags";
|
||||
|
||||
import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component";
|
||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
|
||||
import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
|
||||
@@ -156,6 +157,12 @@ const routes: Routes = [
|
||||
canActivate: [UnauthGuard],
|
||||
data: { titleId: "deleteAccount" },
|
||||
},
|
||||
{
|
||||
path: "verify-recover-delete-provider",
|
||||
component: VerifyRecoverDeleteProviderComponent,
|
||||
canActivate: [UnauthGuard],
|
||||
data: { titleId: "deleteAccount" },
|
||||
},
|
||||
{
|
||||
path: "send/:sendId/:key",
|
||||
component: AccessComponent,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f
|
||||
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component";
|
||||
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component";
|
||||
import { ProvidersComponent } from "../admin-console/providers/providers.component";
|
||||
import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component";
|
||||
import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component";
|
||||
import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component";
|
||||
import { AcceptOrganizationComponent } from "../auth/accept-organization.component";
|
||||
@@ -184,6 +185,7 @@ import { SharedModule } from "./shared.module";
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
VerifyRecoverDeleteProviderComponent,
|
||||
LowKdfComponent,
|
||||
],
|
||||
exports: [
|
||||
@@ -261,6 +263,7 @@ import { SharedModule } from "./shared.module";
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
VerifyRecoverDeleteProviderComponent,
|
||||
LowKdfComponent,
|
||||
HeaderModule,
|
||||
DangerZoneComponent,
|
||||
|
||||
@@ -124,6 +124,9 @@ export class CollectionAdminService {
|
||||
view.groups = c.groups;
|
||||
view.users = c.users;
|
||||
view.assigned = c.assigned;
|
||||
view.readOnly = c.readOnly;
|
||||
view.hidePasswords = c.hidePasswords;
|
||||
view.manage = c.manage;
|
||||
}
|
||||
|
||||
return view;
|
||||
|
||||
@@ -7905,5 +7905,44 @@
|
||||
},
|
||||
"unassignedItemsBannerSelfHost": {
|
||||
"message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
|
||||
},
|
||||
"restrictedGroupAccessDesc": {
|
||||
"message": "You cannot add yourself to a group."
|
||||
},
|
||||
"deleteProvider": {
|
||||
"message": "Delete provider"
|
||||
},
|
||||
"deleteProviderConfirmation": {
|
||||
"message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data."
|
||||
},
|
||||
"deleteProviderName": {
|
||||
"message": "Cannot delete $ID$",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
"example": "John Smith"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deleteProviderWarningDesc": {
|
||||
"message": "You must unlink all clients before you can delete $ID$",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
"example": "John Smith"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providerDeleted": {
|
||||
"message": "Provider deleted"
|
||||
},
|
||||
"providerDeletedDesc": {
|
||||
"message": "The Provider and all associated data has been deleted."
|
||||
},
|
||||
"deleteProviderRecoverConfirmDesc": {
|
||||
"message": "You have requested to delete this Provider. Use the button below to confirm."
|
||||
},
|
||||
"deleteProviderWarning": {
|
||||
"message": "Deleting your provider is permanent. It cannot be undone."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau
|
||||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||
|
||||
import { DangerZoneComponent } from "../../../../../../apps/web/src/app/auth/settings/account/danger-zone.component";
|
||||
import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
|
||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
|
||||
|
||||
@@ -40,6 +41,7 @@ import { SetupComponent } from "./setup/setup.component";
|
||||
ProvidersLayoutComponent,
|
||||
PaymentMethodWarningsModule,
|
||||
TaxInfoComponent,
|
||||
DangerZoneComponent,
|
||||
],
|
||||
declarations: [
|
||||
AcceptProviderComponent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<bit-container>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
@@ -49,3 +49,10 @@
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<app-danger-zone *ngIf="enableDeleteProvider$ | async">
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteProvider()">
|
||||
{{ "deleteProvider" | i18n }}
|
||||
</button>
|
||||
</app-danger-zone>
|
||||
</bit-container>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request";
|
||||
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "provider-account",
|
||||
@@ -23,6 +28,11 @@ export class AccountComponent {
|
||||
|
||||
private providerId: string;
|
||||
|
||||
protected enableDeleteProvider$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableDeleteProvider,
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
@@ -30,6 +40,9 @@ export class AccountComponent {
|
||||
private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -38,7 +51,7 @@ export class AccountComponent {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
try {
|
||||
this.provider = await this.apiService.getProvider(this.providerId);
|
||||
this.provider = await this.providerApiService.getProvider(this.providerId);
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
@@ -53,7 +66,7 @@ export class AccountComponent {
|
||||
request.businessName = this.provider.businessName;
|
||||
request.billingEmail = this.provider.billingEmail;
|
||||
|
||||
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
|
||||
this.formPromise = this.providerApiService.putProvider(this.providerId, request).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
await this.formPromise;
|
||||
@@ -62,4 +75,60 @@ export class AccountComponent {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteProvider() {
|
||||
const providerClients = await this.apiService.getProviderClients(this.providerId);
|
||||
if (providerClients.data != null && providerClients.data.length > 0) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteProviderName", placeholders: [this.provider.name] },
|
||||
content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] },
|
||||
acceptButtonText: { key: "ok" },
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const userVerified = await this.verifyUser();
|
||||
if (!userVerified) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.formPromise = this.providerApiService.deleteProvider(this.providerId);
|
||||
try {
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("providerDeleted"),
|
||||
this.i18nService.t("providerDeletedDesc"),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.formPromise = null;
|
||||
}
|
||||
|
||||
private async verifyUser(): Promise<boolean> {
|
||||
const confirmDescription = "deleteProviderConfirmation";
|
||||
const result = await UserVerificationDialogComponent.open(this.dialogService, {
|
||||
title: "deleteProvider",
|
||||
bodyText: confirmDescription,
|
||||
confirmButtonOptions: {
|
||||
text: "deleteProvider",
|
||||
type: "danger",
|
||||
},
|
||||
});
|
||||
|
||||
// Handle the result of the dialog based on user action and verification success
|
||||
if (result.userAction === "cancel") {
|
||||
// User cancelled the dialog
|
||||
return false;
|
||||
}
|
||||
|
||||
// User confirmed the dialog so check verification success
|
||||
if (!result.verificationSuccess) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -50,10 +50,10 @@ export class SetupComponent implements OnInit {
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private cryptoService: CryptoService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
private validationService: ValidationService,
|
||||
private configService: ConfigService,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -80,7 +80,7 @@ export class SetupComponent implements OnInit {
|
||||
|
||||
// Check if provider exists, redirect if it does
|
||||
try {
|
||||
const provider = await this.apiService.getProvider(this.providerId);
|
||||
const provider = await this.providerApiService.getProvider(this.providerId);
|
||||
if (provider.name != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@@ -128,7 +128,7 @@ export class SetupComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
const provider = await this.apiService.postProviderSetup(this.providerId, request);
|
||||
const provider = await this.providerApiService.postProviderSetup(this.providerId, request);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
InternalPolicyService,
|
||||
PolicyService as PolicyServiceAbstraction,
|
||||
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
||||
@@ -47,6 +48,7 @@ import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/comm
|
||||
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
|
||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import {
|
||||
@@ -1115,6 +1117,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: LoggingErrorHandler,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ProviderApiServiceAbstraction,
|
||||
useClass: ProviderApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
function encryptServiceFactory(
|
||||
|
||||
@@ -4,8 +4,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re
|
||||
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
||||
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
|
||||
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
|
||||
import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request";
|
||||
import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request";
|
||||
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
|
||||
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
|
||||
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
|
||||
@@ -29,7 +27,6 @@ import {
|
||||
ProviderUserResponse,
|
||||
ProviderUserUserDetailsResponse,
|
||||
} from "../admin-console/models/response/provider/provider-user.response";
|
||||
import { ProviderResponse } from "../admin-console/models/response/provider/provider.response";
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
@@ -297,7 +294,6 @@ export abstract class ApiService {
|
||||
) => Promise<any>;
|
||||
|
||||
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
|
||||
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
|
||||
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
|
||||
|
||||
getSync: () => Promise<SyncResponse>;
|
||||
@@ -373,10 +369,6 @@ export abstract class ApiService {
|
||||
getPlans: () => Promise<ListResponse<PlanResponse>>;
|
||||
getTaxRates: () => Promise<ListResponse<TaxRateResponse>>;
|
||||
|
||||
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
|
||||
getProvider: (id: string) => Promise<ProviderResponse>;
|
||||
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
|
||||
|
||||
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
|
||||
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;
|
||||
postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
|
||||
import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request";
|
||||
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
|
||||
import { ProviderResponse } from "../../models/response/provider/provider.response";
|
||||
|
||||
export class ProviderApiServiceAbstraction {
|
||||
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
|
||||
getProvider: (id: string) => Promise<ProviderResponse>;
|
||||
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
|
||||
providerRecoverDeleteToken: (
|
||||
organizationId: string,
|
||||
request: ProviderVerifyRecoverDeleteRequest,
|
||||
) => Promise<any>;
|
||||
deleteProvider: (id: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class ProviderVerifyRecoverDeleteRequest {
|
||||
token: string;
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction";
|
||||
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
|
||||
import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request";
|
||||
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
|
||||
import { ProviderResponse } from "../../models/response/provider/provider.response";
|
||||
|
||||
export class ProviderApiService implements ProviderApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
async postProviderSetup(id: string, request: ProviderSetupRequest) {
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + id + "/setup",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ProviderResponse(r);
|
||||
}
|
||||
|
||||
async getProvider(id: string) {
|
||||
const r = await this.apiService.send("GET", "/providers/" + id, null, true, true);
|
||||
return new ProviderResponse(r);
|
||||
}
|
||||
|
||||
async putProvider(id: string, request: ProviderUpdateRequest) {
|
||||
const r = await this.apiService.send("PUT", "/providers/" + id, request, true, true);
|
||||
return new ProviderResponse(r);
|
||||
}
|
||||
|
||||
providerRecoverDeleteToken(
|
||||
providerId: string,
|
||||
request: ProviderVerifyRecoverDeleteRequest,
|
||||
): Promise<any> {
|
||||
return this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + providerId + "/delete-recover-token",
|
||||
request,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteProvider(id: string): Promise<void> {
|
||||
await this.apiService.send("DELETE", "/providers/" + id, null, true, false);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export enum FeatureFlag {
|
||||
EnableConsolidatedBilling = "enable-consolidated-billing",
|
||||
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
||||
UnassignedItemsBanner = "unassigned-items-banner",
|
||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||
}
|
||||
|
||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||
|
||||
@@ -7,8 +7,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re
|
||||
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
||||
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
|
||||
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
|
||||
import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request";
|
||||
import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request";
|
||||
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
|
||||
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
|
||||
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
|
||||
@@ -32,7 +30,6 @@ import {
|
||||
ProviderUserResponse,
|
||||
ProviderUserUserDetailsResponse,
|
||||
} from "../admin-console/models/response/provider/provider-user.response";
|
||||
import { ProviderResponse } from "../admin-console/models/response/provider/provider.response";
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { TokenService } from "../auth/abstractions/token.service";
|
||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||
@@ -866,16 +863,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return r;
|
||||
}
|
||||
|
||||
async putGroupUsers(organizationId: string, id: string, request: string[]): Promise<any> {
|
||||
await this.send(
|
||||
"PUT",
|
||||
"/organizations/" + organizationId + "/groups/" + id + "/users",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> {
|
||||
return this.send(
|
||||
"DELETE",
|
||||
@@ -1161,23 +1148,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return this.send("DELETE", "/organizations/connections/" + id, null, true, false);
|
||||
}
|
||||
|
||||
// Provider APIs
|
||||
|
||||
async postProviderSetup(id: string, request: ProviderSetupRequest) {
|
||||
const r = await this.send("POST", "/providers/" + id + "/setup", request, true, true);
|
||||
return new ProviderResponse(r);
|
||||
}
|
||||
|
||||
async getProvider(id: string) {
|
||||
const r = await this.send("GET", "/providers/" + id, null, true, true);
|
||||
return new ProviderResponse(r);
|
||||
}
|
||||
|
||||
async putProvider(id: string, request: ProviderUpdateRequest) {
|
||||
const r = await this.send("PUT", "/providers/" + id, request, true, true);
|
||||
return new ProviderResponse(r);
|
||||
}
|
||||
|
||||
// Provider User APIs
|
||||
|
||||
async getProviderUsers(
|
||||
|
||||
@@ -21,6 +21,10 @@ export class CollectionDetailsResponse extends CollectionResponse {
|
||||
readOnly: boolean;
|
||||
manage: boolean;
|
||||
hidePasswords: boolean;
|
||||
|
||||
/**
|
||||
* Flag indicating the user has been explicitly assigned to this Collection
|
||||
*/
|
||||
assigned: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
@@ -35,15 +39,10 @@ export class CollectionDetailsResponse extends CollectionResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionAccessDetailsResponse extends CollectionResponse {
|
||||
export class CollectionAccessDetailsResponse extends CollectionDetailsResponse {
|
||||
groups: SelectionReadOnlyResponse[] = [];
|
||||
users: SelectionReadOnlyResponse[] = [];
|
||||
|
||||
/**
|
||||
* Flag indicating the user has been explicitly assigned to this Collection
|
||||
*/
|
||||
assigned: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.assigned = this.getResponseProperty("Assigned") || false;
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -247,7 +247,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.4.0"
|
||||
"version": "2024.4.1"
|
||||
},
|
||||
"libs/admin-console": {
|
||||
"name": "@bitwarden/admin-console",
|
||||
|
||||
Reference in New Issue
Block a user