1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +01:00

[AC-2595] [AC-2596] Empty clients placeholder and setup provider hint (#9505)

* Added empty state to providers clients page

* Added bitForm to Setup component and added billing email hint
This commit is contained in:
Alex Morask 2024-06-11 10:36:31 -04:00 committed by GitHub
parent 9a35608fc3
commit f6702cd2d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 304 additions and 203 deletions

View File

@ -8330,5 +8330,12 @@
},
"viewSecret": {
"message": "View secret"
},
"noClients": {
"message": "There are no clients to list"
},
"providerBillingEmailHint": {
"message": "This email address will receive all invoices pertaining to this provider",
"description": "A hint that shows up on the Provider setup page to inform the admin the billing email will receive the provider's invoices."
}
}

View File

@ -11,6 +11,7 @@ import { OssModule } from "@bitwarden/web-vault/app/oss.module";
import {
CreateClientOrganizationComponent,
NoClientsComponent,
ManageClientOrganizationNameComponent,
ManageClientOrganizationsComponent,
ManageClientOrganizationSubscriptionComponent,
@ -65,6 +66,7 @@ import { SetupComponent } from "./setup/setup.component";
SetupProviderComponent,
UserAddEditComponent,
CreateClientOrganizationComponent,
NoClientsComponent,
ManageClientOrganizationsComponent,
ManageClientOrganizationNameComponent,
ManageClientOrganizationSubscriptionComponent,

View File

@ -1,40 +1,41 @@
<app-payment-method-warnings
*ngIf="showPaymentMethodWarningBanners$ | async"
></app-payment-method-warnings>
<div class="container page-content">
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<div class="container page-content" *ngIf="!loading">
<div class="page-header">
<h1>{{ "setupProvider" | i18n }}</h1>
</div>
<p>{{ "setupProviderDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
<form [formGroup]="formGroup" [bitSubmit]="submit">
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
<div class="row">
<div class="form-group col-6">
<label for="name">{{ "providerName" | i18n }}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
<div class="tw-grid tw-grid-flow-col tw-grid-cols-12 tw-gap-4">
<div class="tw-col-span-6">
<bit-form-field>
<bit-label>{{ "providerName" | i18n }}</bit-label>
<input type="text" bitInput formControlName="name" />
</bit-form-field>
</div>
<div class="form-group col-6">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="billingEmail"
required
/>
</div>
<div *ngIf="enableConsolidatedBilling$ | async" class="form-group col-12">
<app-tax-info />
<div class="tw-col-span-6">
<bit-form-field>
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input type="email" bitInput formControlName="billingEmail" />
<bit-hint *ngIf="enableConsolidatedBilling$ | async">{{
"providerBillingEmailHint" | i18n
}}</bit-hint>
</bit-form-field>
</div>
</div>
<div class="mt-4">
<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>
</div>
<app-manage-tax-information *ngIf="enableConsolidatedBilling$ | async" />
<button bitButton bitFormButton buttonType="primary" type="submit">
{{ "submit" | i18n }}
</button>
</form>
</div>

View File

@ -1,38 +1,41 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { firstValueFrom, Subject, switchMap } from "rxjs";
import { first, takeUntil } from "rxjs/operators";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
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 { TaxInformation } from "@bitwarden/common/billing/models/domain";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { ProviderKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { TaxInfoComponent } from "@bitwarden/web-vault/app/billing";
import { ToastService } from "@bitwarden/components";
@Component({
selector: "provider-setup",
templateUrl: "setup.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SetupComponent implements OnInit {
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
export class SetupComponent implements OnInit, OnDestroy {
@ViewChild(ManageTaxInformationComponent)
manageTaxInformationComponent: ManageTaxInformationComponent;
loading = true;
authed = false;
email: string;
formPromise: Promise<any>;
providerId: string;
token: string;
name: string;
billingEmail: string;
protected formGroup = this.formBuilder.group({
name: ["", Validators.required],
billingEmail: ["", [Validators.required, Validators.email]],
});
protected readonly TaxInformation = TaxInformation;
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
FeatureFlag.ShowPaymentMethodWarningBanners,
@ -42,9 +45,10 @@ export class SetupComponent implements OnInit {
FeatureFlag.EnableConsolidatedBilling,
);
private destroy$ = new Subject<void>();
constructor(
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private route: ActivatedRoute,
private cryptoService: CryptoService,
@ -52,61 +56,81 @@ export class SetupComponent implements OnInit {
private validationService: ValidationService,
private configService: ConfigService,
private providerApiService: ProviderApiServiceAbstraction,
private formBuilder: FormBuilder,
private toastService: ToastService,
) {}
ngOnInit() {
document.body.classList.remove("layout_frontend");
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
if (error) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("emergencyInviteAcceptFailed"),
{ timeout: 10000 },
);
// 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.router.navigate(["/"]);
this.route.queryParams
.pipe(
first(),
switchMap(async (queryParams) => {
const error =
queryParams.providerId == null ||
queryParams.email == null ||
queryParams.token == null;
if (error) {
this.toastService.showToast({
variant: "error",
title: null,
message: this.i18nService.t("emergencyInviteAcceptFailed"),
timeout: 10000,
});
return await this.router.navigate(["/"]);
}
this.providerId = queryParams.providerId;
this.token = queryParams.token;
try {
const provider = await this.providerApiService.getProvider(this.providerId);
if (provider.name != null) {
/*
This is currently always going to result in a redirect to the Vault because the `provider-permissions.guard`
checks for the existence of the Provider in state. However, when accessing the Setup page via the email link,
this `navigate` invocation will be hit before the sync can complete, thus resulting in a null Provider. If we want
to resolve it, we'd either need to use the ProviderApiService in the provider-permissions.guard (added expense)
or somehow check that the previous route was /setup.
*/
return await this.router.navigate(["/providers", provider.id], {
replaceUrl: true,
});
}
this.loading = false;
} catch (error) {
this.validationService.showError(error);
return await this.router.navigate(["/"]);
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
submit = async () => {
try {
this.formGroup.markAllAsTouched();
const taxInformationValid = this.manageTaxInformationComponent.touch();
if (this.formGroup.invalid || !taxInformationValid) {
return;
}
this.providerId = qParams.providerId;
this.token = qParams.token;
// Check if provider exists, redirect if it does
try {
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
this.router.navigate(["/providers", provider.id], { replaceUrl: true });
}
} catch (e) {
this.validationService.showError(e);
// 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.router.navigate(["/"]);
}
});
}
async submit() {
this.formPromise = this.doSubmit();
await this.formPromise;
this.formPromise = null;
}
async doSubmit() {
try {
const providerKey = await this.cryptoService.makeOrgKey<ProviderKey>();
const key = providerKey[0].encryptedString;
const request = new ProviderSetupRequest();
request.name = this.name;
request.billingEmail = this.billingEmail;
request.name = this.formGroup.value.name;
request.billingEmail = this.formGroup.value.billingEmail;
request.token = this.token;
request.key = key;
@ -114,27 +138,32 @@ export class SetupComponent implements OnInit {
if (enableConsolidatedBilling) {
request.taxInfo = new ExpandedTaxInfoUpdateRequest();
const taxInfoView = this.taxInfoComponent.taxInfo;
request.taxInfo.country = taxInfoView.country;
request.taxInfo.postalCode = taxInfoView.postalCode;
if (taxInfoView.includeTaxId) {
request.taxInfo.taxId = taxInfoView.taxId;
request.taxInfo.line1 = taxInfoView.line1;
request.taxInfo.line2 = taxInfoView.line2;
request.taxInfo.city = taxInfoView.city;
request.taxInfo.state = taxInfoView.state;
const taxInformation = this.manageTaxInformationComponent.getTaxInformation();
request.taxInfo.country = taxInformation.country;
request.taxInfo.postalCode = taxInformation.postalCode;
if (taxInformation.includeTaxId) {
request.taxInfo.taxId = taxInformation.taxId;
request.taxInfo.line1 = taxInformation.line1;
request.taxInfo.line2 = taxInformation.line2;
request.taxInfo.city = taxInformation.city;
request.taxInfo.state = taxInformation.state;
}
}
const provider = await this.providerApiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("providerSetup"),
});
await this.syncService.fullSync(true);
// 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.router.navigate(["/providers", provider.id]);
await this.router.navigate(["/providers", provider.id]);
} catch (e) {
this.validationService.showError(e);
}
}
};
}

View File

@ -2,3 +2,4 @@ export * from "./create-client-organization.component";
export * from "./manage-client-organizations.component";
export * from "./manage-client-organization-name.component";
export * from "./manage-client-organization-subscription.component";
export * from "./no-clients.component";

View File

@ -21,80 +21,77 @@
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="!loading && (clients | search: searchText : 'organizationName' : 'id') as searchedClients"
>
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
<ng-container *ngIf="searchedClients.length">
<bit-table
*ngIf="searchedClients?.length >= 1"
[dataSource]="dataSource"
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th colspan="2" bitCell bitSortable="organizationName" default>{{ "client" | i18n }}</th>
<th bitCell bitSortable="seats">{{ "assigned" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "used" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "remaining" | i18n }}</th>
<th bitCell bitSortable="plan">{{ "billingPlan" | i18n }}</th>
<th></th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let client of rows$ | async">
<td bitCell width="30">
<bit-avatar [text]="client.organizationName" [id]="client.id" size="small"></bit-avatar>
</td>
<td bitCell>
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
<a bitLink [routerLink]="['/organizations', client.organizationId]">{{
client.organizationName
}}</a>
</div>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.userCount }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats - client.userCount }}</span>
</td>
<td>
<span>{{ client.plan }}</span>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="manageName(client)">
<i aria-hidden="true" class="bwi bwi-pencil-square"></i>
{{ "updateName" | i18n }}
</button>
<button type="button" bitMenuItem (click)="manageSubscription(client)">
<i aria-hidden="true" class="bwi bwi-family"></i>
{{ "manageSubscription" | i18n }}
</button>
<button type="button" bitMenuItem (click)="remove(client)">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "unlinkOrganization" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
<ng-container *ngIf="!loading">
<bit-table
[dataSource]="dataSource"
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th colspan="2" bitCell bitSortable="organizationName" default>{{ "client" | i18n }}</th>
<th bitCell bitSortable="seats">{{ "assigned" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "used" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "remaining" | i18n }}</th>
<th bitCell bitSortable="plan">{{ "billingPlan" | i18n }}</th>
<th></th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let client of rows$ | async">
<td bitCell width="30">
<bit-avatar [text]="client.organizationName" [id]="client.id" size="small"></bit-avatar>
</td>
<td bitCell>
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
<a bitLink [routerLink]="['/organizations', client.organizationId]">{{
client.organizationName
}}</a>
</div>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.userCount }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats - client.userCount }}</span>
</td>
<td>
<span>{{ client.plan }}</span>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="manageName(client)">
<i aria-hidden="true" class="bwi bwi-pencil-square"></i>
{{ "updateName" | i18n }}
</button>
<button type="button" bitMenuItem (click)="manageSubscription(client)">
<i aria-hidden="true" class="bwi bwi-family"></i>
{{ "manageSubscription" | i18n }}
</button>
<button type="button" bitMenuItem (click)="remove(client)">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "unlinkOrganization" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
<div *ngIf="clients.length === 0" class="tw-mt-10">
<app-no-clients (addNewOrganizationClicked)="createClientOrganization()" />
</div>
</ng-container>

View File

@ -0,0 +1,40 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { svgIcon } from "@bitwarden/components";
const gearIcon = svgIcon`
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.9995 37.9541C46.4641 37.9541 35.5465 48.6298 35.5465 61.7321C35.5465 74.8343 46.4641 85.51 59.9995 85.51C73.5349 85.51 84.4526 74.8343 84.4526 61.7321C84.4526 48.6298 73.5349 37.9541 59.9995 37.9541ZM33.1465 61.7321C33.1465 47.2444 45.1994 35.5541 59.9995 35.5541C74.7997 35.5541 86.8526 47.2444 86.8526 61.7321C86.8526 76.2197 74.7997 87.91 59.9995 87.91C45.1994 87.91 33.1465 76.2197 33.1465 61.7321Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M98.9992 8.4C94.36 8.4 90.5992 12.1608 90.5992 16.8C90.5992 21.4392 94.36 25.2 98.9992 25.2C103.638 25.2 107.399 21.4392 107.399 16.8C107.399 12.1608 103.638 8.4 98.9992 8.4ZM88.1992 16.8C88.1992 10.8353 93.0345 6 98.9992 6C104.964 6 109.799 10.8353 109.799 16.8C109.799 22.7647 104.964 27.6 98.9992 27.6C93.0345 27.6 88.1992 22.7647 88.1992 16.8Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M109.2 56.4C104.561 56.4 100.8 60.1608 100.8 64.8C100.8 69.4392 104.561 73.2 109.2 73.2C113.84 73.2 117.6 69.4392 117.6 64.8C117.6 60.1608 113.84 56.4 109.2 56.4ZM98.4004 64.8C98.4004 58.8353 103.236 54 109.2 54C115.165 54 120 58.8353 120 64.8C120 70.7647 115.165 75.6 109.2 75.6C103.236 75.6 98.4004 70.7647 98.4004 64.8Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M100.8 99C96.1608 99 92.4 102.761 92.4 107.4C92.4 112.039 96.1608 115.8 100.8 115.8C105.439 115.8 109.2 112.039 109.2 107.4C109.2 102.761 105.439 99 100.8 99ZM90 107.4C90 101.435 94.8353 96.6 100.8 96.6C106.765 96.6 111.6 101.435 111.6 107.4C111.6 113.365 106.765 118.2 100.8 118.2C94.8353 118.2 90 113.365 90 107.4Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.8 98.4C33.1608 98.4 29.4 102.161 29.4 106.8C29.4 111.439 33.1608 115.2 37.8 115.2C42.4392 115.2 46.2 111.439 46.2 106.8C46.2 102.161 42.4392 98.4 37.8 98.4ZM27 106.8C27 100.835 31.8353 96 37.8 96C43.7647 96 48.6 100.835 48.6 106.8C48.6 112.765 43.7647 117.6 37.8 117.6C31.8353 117.6 27 112.765 27 106.8Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8 40.2C6.16081 40.2 2.4 43.9608 2.4 48.6C2.4 53.2392 6.16081 57 10.8 57C15.4392 57 19.2 53.2392 19.2 48.6C19.2 43.9608 15.4392 40.2 10.8 40.2ZM0 48.6C0 42.6353 4.83532 37.8 10.8 37.8C16.7647 37.8 21.6 42.6353 21.6 48.6C21.6 54.5647 16.7647 59.4 10.8 59.4C4.83532 59.4 0 54.5647 0 48.6Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.3996 3.60001C33.7604 3.60001 29.9996 7.36082 29.9996 12C29.9996 16.6392 33.7604 20.4 38.3996 20.4C43.0388 20.4 46.7996 16.6392 46.7996 12C46.7996 7.36082 43.0388 3.60001 38.3996 3.60001ZM27.5996 12C27.5996 6.03534 32.4349 1.20001 38.3996 1.20001C44.3643 1.20001 49.1996 6.03534 49.1996 12C49.1996 17.9647 44.3643 22.8 38.3996 22.8C32.4349 22.8 27.5996 17.9647 27.5996 12Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.217 21.3484C42.5229 21.221 42.8742 21.3656 43.0017 21.6715L49.7525 37.8734C49.8799 38.1793 49.7353 38.5306 49.4294 38.6581C49.1235 38.7855 48.7722 38.6409 48.6448 38.335L41.894 22.133C41.7665 21.8272 41.9112 21.4759 42.217 21.3484Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M92.7905 24.1445C93.0435 24.3585 93.075 24.7371 92.861 24.9901L78.0092 42.5422C77.7952 42.7951 77.4166 42.8267 77.1636 42.6126C76.9107 42.3986 76.8791 42.02 77.0932 41.767L91.9449 24.2149C92.159 23.962 92.5375 23.9304 92.7905 24.1445Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.4265 51.4253C20.523 51.1083 20.8582 50.9295 21.1752 51.026L34.9752 55.226C35.2923 55.3225 35.471 55.6577 35.3746 55.9747C35.2781 56.2917 34.9429 56.4705 34.6259 56.374L20.8259 52.174C20.5088 52.0776 20.3301 51.7424 20.4265 51.4253Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.4777 84.0684C49.7714 84.2219 49.8849 84.5845 49.7314 84.8781L42.9795 97.7892C42.8259 98.0829 42.4634 98.1964 42.1697 98.0429C41.8761 97.8893 41.7625 97.5268 41.9161 97.2331L48.668 84.322C48.8216 84.0284 49.1841 83.9148 49.4777 84.0684Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M77.1582 79.5058C77.4086 79.2888 77.7876 79.3159 78.0046 79.5663L95.5567 99.8187C95.7737 100.069 95.7466 100.448 95.4962 100.665C95.2458 100.882 94.8669 100.855 94.6499 100.605L77.0978 80.3522C76.8807 80.1018 76.9078 79.7229 77.1582 79.5058Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M85.0558 62.3473C85.0887 62.0176 85.3828 61.7771 85.7125 61.81L99.2141 63.1602C99.5438 63.1932 99.7844 63.4872 99.7514 63.8169C99.7184 64.1466 99.4244 64.3872 99.0947 64.3542L85.5931 63.0041C85.2634 62.9711 85.0228 62.6771 85.0558 62.3473Z" fill="#CED4DC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.0583 45.4382C54.888 45.247 54.615 45.185 54.3788 45.2838L52.1688 46.2079C51.9281 46.3086 51.7801 46.5531 51.8024 46.8129L52.1362 50.6898C52.1819 51.2204 51.9902 51.744 51.6128 52.1197L50.2505 53.4761C49.894 53.8311 49.4052 54.0206 48.9027 53.9989L45.0074 53.8303C44.7569 53.8194 44.5261 53.9655 44.4286 54.1965L43.4934 56.4137C43.3921 56.6536 43.4573 56.9315 43.6545 57.1014L46.5356 59.5838C46.9324 59.9257 47.1606 60.4236 47.1606 60.9474V62.8948C47.1606 63.4058 46.9435 63.8927 46.5633 64.2341L43.7142 66.7927C43.5238 66.9636 43.4628 67.2365 43.5622 67.4723L44.4892 69.6698C44.5905 69.9098 44.835 70.0571 45.0945 70.0343L48.8457 69.7047C49.3746 69.6583 49.897 69.8477 50.2732 70.2223L51.6345 71.5776C51.9931 71.9346 52.1849 72.4261 52.1628 72.9317L51.994 76.7976C51.983 77.0488 52.1299 77.2803 52.3619 77.3773L54.5876 78.308C54.8286 78.4088 55.1071 78.3421 55.2763 78.143L57.6994 75.2918C58.0414 74.8894 58.5429 74.6575 59.071 74.6575H61.0298C61.5381 74.6575 62.0226 74.8723 62.3639 75.249L64.9399 78.0926C65.1106 78.281 65.3815 78.3414 65.616 78.2433L67.8309 77.3171C68.072 77.2163 68.2202 76.9709 68.1971 76.7106L67.8673 72.9892C67.8201 72.4571 68.0117 71.9316 68.3903 71.5547L69.7513 70.1996C70.1078 69.8447 70.5965 69.6551 71.0991 69.6769L74.9944 69.8455C75.2449 69.8563 75.4757 69.7103 75.5731 69.4793L76.5045 67.2715C76.6066 67.0293 76.5393 66.7488 76.3382 66.5794L73.4814 64.1727C73.0755 63.8307 72.8412 63.3269 72.8412 62.7961V60.856C72.8412 60.3457 73.0578 59.8594 73.4371 59.518L76.3646 56.8836C76.5546 56.7125 76.6154 56.4399 76.5161 56.2043L75.5887 54.006C75.4875 53.766 75.2429 53.6187 74.9834 53.6415L71.2322 53.9711C70.7034 54.0175 70.181 53.8281 69.8047 53.4535L68.4434 52.0982C68.0848 51.7411 67.8931 51.2496 67.9152 50.7441L68.084 46.8782C68.0949 46.627 67.9481 46.3955 67.716 46.2985L65.4903 45.3678C65.2493 45.267 64.9708 45.3337 64.8016 45.5328L62.3785 48.384C62.0365 48.7864 61.5351 49.0183 61.007 49.0183H59.0542C58.5406 49.0183 58.0516 48.799 57.71 48.4155L55.0583 45.4382ZM53.9158 44.1767C54.6246 43.8803 55.4434 44.0664 55.9544 44.6401L58.6061 47.6174C58.72 47.7452 58.883 47.8183 59.0542 47.8183H61.007C61.183 47.8183 61.3502 47.741 61.4642 47.6069L63.8872 44.7557C64.3947 44.1585 65.2303 43.9583 65.9532 44.2607L68.179 45.1914C68.8751 45.4825 69.3157 46.1768 69.2828 46.9306L69.114 50.7964C69.1067 50.965 69.1706 51.1288 69.2901 51.2478L70.6514 52.6031C70.7768 52.728 70.9509 52.7911 71.1272 52.7757L74.8784 52.4461C75.6569 52.3777 76.3906 52.8195 76.6944 53.5396L77.6217 55.7379C77.9198 56.4446 77.7374 57.2625 77.1673 57.7755L74.2398 60.41C74.1134 60.5238 74.0412 60.6859 74.0412 60.856V62.7961C74.0412 62.973 74.1193 63.1409 74.2546 63.2549L77.1114 65.6617C77.7145 66.1698 77.9166 67.0113 77.6101 67.7379L76.6788 69.9457C76.3864 70.6387 75.694 71.0769 74.9425 71.0444L71.0472 70.8758C70.8797 70.8685 70.7168 70.9317 70.598 71.05L69.2369 72.4051C69.1108 72.5307 69.0469 72.7059 69.0626 72.8833L69.3924 76.6046C69.4616 77.3857 69.0173 78.1217 68.2939 78.4242L66.079 79.3504C65.3753 79.6447 64.5626 79.4635 64.0505 78.8982L61.4745 76.0547C61.3608 75.9291 61.1993 75.8575 61.0298 75.8575H59.071C58.8949 75.8575 58.7278 75.9348 58.6138 76.0689L56.1907 78.9201C55.6832 79.5173 54.8477 79.7174 54.1247 79.4151L51.899 78.4844C51.2029 78.1933 50.7622 77.499 50.7951 76.7452L50.9639 72.8793C50.9713 72.7108 50.9074 72.547 50.7878 72.428L49.4265 71.0726C49.3011 70.9478 49.127 70.8846 48.9507 70.9001L45.1996 71.2297C44.421 71.298 43.6873 70.8562 43.3836 70.1362L42.4566 67.9387C42.1582 67.2314 42.3412 66.4127 42.9124 65.8998L45.7615 63.3412C45.8883 63.2274 45.9606 63.0651 45.9606 62.8948V60.9474C45.9606 60.7728 45.8846 60.6069 45.7523 60.4929L42.8712 58.0105C42.2794 57.5006 42.0841 56.6671 42.3877 55.9473L43.323 53.7301C43.6153 53.0371 44.3078 52.5989 45.0593 52.6314L48.9546 52.8C49.1221 52.8073 49.285 52.7441 49.4038 52.6258L50.7662 51.2694C50.892 51.1441 50.9558 50.9696 50.9406 50.7927L50.6069 46.9159C50.5398 46.1363 50.9839 45.4027 51.7058 45.1008L53.9158 44.1767ZM65.7734 59.49C64.5008 56.2102 60.7895 54.7187 57.5276 56.0511C54.2534 57.3885 52.7303 61.0753 54.0787 64.2677C55.4244 67.5297 59.1264 69.0401 62.327 67.6984C65.5088 66.3645 67.1209 62.6899 65.7734 59.49ZM64.6574 59.9312C63.6423 57.3035 60.6562 56.0694 57.9814 57.162C55.3169 58.2503 54.0985 61.2338 55.185 63.8029L55.1872 63.808L55.1872 63.808C56.2791 66.4579 59.2771 67.6758 61.863 66.5917C64.4656 65.5006 65.7472 62.5089 64.6645 59.9487C64.662 59.9429 64.6597 59.9371 64.6574 59.9312Z" fill="#CED4DC"/>
</svg>
`;
@Component({
selector: "app-no-clients",
template: `<div class="tw-flex tw-flex-col tw-items-center tw-text-info">
<bit-icon [icon]="icon"></bit-icon>
<p class="tw-mt-4">{{ "noClients" | i18n }}</p>
<a type="button" bitButton buttonType="primary" (click)="addNewOrganization()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "addNewOrganization" | i18n }}
</a>
</div>`,
})
export class NoClientsComponent {
icon = gearIcon;
@Output() addNewOrganizationClicked = new EventEmitter();
addNewOrganization = () => this.addNewOrganizationClicked.emit();
}

View File

@ -1,7 +1,4 @@
export * from "./clients/create-client-organization.component";
export * from "./clients/manage-client-organization-name.component";
export * from "./clients/manage-client-organization-subscription.component";
export * from "./clients/manage-client-organizations.component";
export * from "./clients";
export * from "./guards/has-consolidated-billing.guard";
export * from "./payment-method/provider-select-payment-method-dialog.component";
export * from "./payment-method/provider-payment-method.component";

View File

@ -44,7 +44,7 @@
<p>{{ "taxInformationDesc" | i18n }}</p>
<app-manage-tax-information
*ngIf="taxInformation"
[taxInformation]="taxInformation"
[startWith]="taxInformation"
[onSubmit]="updateTaxInformation"
(taxInformationUpdated)="onDataUpdated()"
/>

View File

@ -1,7 +1,7 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div class="tw-col-span-6">
<bit-form-field>
<bit-form-field [disableMargin]="selectionSupportsAdditionalOptions">
<bit-label>{{ "country" | i18n }}</bit-label>
<bit-select formControlName="country">
<bit-option
@ -14,7 +14,7 @@
</bit-form-field>
</div>
<div class="tw-col-span-6">
<bit-form-field>
<bit-form-field [disableMargin]="selectionSupportsAdditionalOptions">
<bit-label>{{ "zipPostalCode" | i18n }}</bit-label>
<input bitInput type="text" formControlName="postalCode" autocomplete="postal-code" />
</bit-form-field>

View File

@ -1,5 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
@ -13,8 +14,8 @@ type Country = {
selector: "app-manage-tax-information",
templateUrl: "./manage-tax-information.component.html",
})
export class ManageTaxInformationComponent implements OnInit {
@Input({ required: true }) taxInformation: TaxInformation;
export class ManageTaxInformationComponent implements OnInit, OnDestroy {
@Input() startWith: TaxInformation;
@Input() onSubmit?: (taxInformation: TaxInformation) => Promise<void>;
@Output() taxInformationUpdated = new EventEmitter();
@ -29,35 +30,61 @@ export class ManageTaxInformationComponent implements OnInit {
state: "",
});
private destroy$ = new Subject<void>();
private taxInformation: TaxInformation;
constructor(private formBuilder: FormBuilder) {}
submit = async () => {
await this.onSubmit({
country: this.formGroup.value.country,
postalCode: this.formGroup.value.postalCode,
taxId: this.formGroup.value.taxId,
line1: this.formGroup.value.line1,
line2: this.formGroup.value.line2,
city: this.formGroup.value.city,
state: this.formGroup.value.state,
});
getTaxInformation = (): TaxInformation & { includeTaxId: boolean } => ({
...this.taxInformation,
includeTaxId: this.formGroup.value.includeTaxId,
});
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
await this.onSubmit(this.taxInformation);
this.taxInformationUpdated.emit();
};
touch = (): boolean => {
this.formGroup.markAllAsTouched();
return this.formGroup.valid;
};
async ngOnInit() {
if (this.taxInformation) {
if (this.startWith) {
this.formGroup.patchValue({
...this.taxInformation,
...this.startWith,
includeTaxId:
this.countrySupportsTax(this.taxInformation.country) &&
(!!this.taxInformation.taxId ||
!!this.taxInformation.line1 ||
!!this.taxInformation.line2 ||
!!this.taxInformation.city ||
!!this.taxInformation.state),
this.countrySupportsTax(this.startWith.country) &&
(!!this.startWith.taxId ||
!!this.startWith.line1 ||
!!this.startWith.line2 ||
!!this.startWith.city ||
!!this.startWith.state),
});
}
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => {
this.taxInformation = {
country: values.country,
postalCode: values.postalCode,
taxId: values.taxId,
line1: values.line1,
line2: values.line2,
city: values.city,
state: values.state,
};
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
protected countrySupportsTax(countryCode: string) {