mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-06 18:57:56 +01:00
Merge branch 'main' of github.com:bitwarden/clients
This commit is contained in:
commit
cf802fc32c
@ -4,25 +4,6 @@
|
||||
[title]="'autofillSuggestions' | i18n"
|
||||
[showRefresh]="showRefresh"
|
||||
(onRefresh)="refreshCurrentTab()"
|
||||
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"
|
||||
showAutofillButton
|
||||
></app-vault-list-items-container>
|
||||
<ng-container *ngIf="showEmptyAutofillTip$ | async">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ "autofillSuggestions" | i18n }}
|
||||
</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
size="small"
|
||||
type="button"
|
||||
[appA11yTitle]="'refresh' | i18n"
|
||||
(click)="refreshCurrentTab()"
|
||||
></button>
|
||||
</bit-section-header>
|
||||
<span class="tw-text-muted tw-px-1" bitTypography="body2">{{
|
||||
"autofillSuggestionsTip" | i18n
|
||||
}}</span>
|
||||
</bit-section>
|
||||
</ng-container>
|
||||
|
@ -3,6 +3,7 @@ import { Component } from "@angular/core";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
IconButtonModule,
|
||||
SectionComponent,
|
||||
@ -45,7 +46,7 @@ export class AutofillVaultListItemsComponent {
|
||||
|
||||
/**
|
||||
* Observable that determines whether the empty autofill tip should be shown.
|
||||
* The tip is shown when there are no ciphers to autofill, no filter is applied, and autofill is allowed in
|
||||
* The tip is shown when there are no login ciphers to autofill, no filter is applied, and autofill is allowed in
|
||||
* the current context (e.g. not in a popout).
|
||||
* @protected
|
||||
*/
|
||||
@ -54,7 +55,10 @@ export class AutofillVaultListItemsComponent {
|
||||
this.autofillCiphers$,
|
||||
this.vaultPopupItemsService.autofillAllowed$,
|
||||
]).pipe(
|
||||
map(([hasFilter, ciphers, canAutoFill]) => !hasFilter && canAutoFill && ciphers.length === 0),
|
||||
map(
|
||||
([hasFilter, ciphers, canAutoFill]) =>
|
||||
!hasFilter && canAutoFill && ciphers.filter((c) => c.type == CipherType.Login).length === 0,
|
||||
),
|
||||
);
|
||||
|
||||
constructor(private vaultPopupItemsService: VaultPopupItemsService) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
<bit-section *ngIf="ciphers?.length > 0">
|
||||
<bit-section *ngIf="ciphers?.length > 0 || description">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
@ -13,6 +13,9 @@
|
||||
></button>
|
||||
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
|
||||
</bit-section-header>
|
||||
<div *ngIf="description" class="tw-text-muted tw-px-1 tw-mb-2" bitTypography="body2">
|
||||
{{ description }}
|
||||
</div>
|
||||
<bit-item-group>
|
||||
<bit-item *ngFor="let cipher of ciphers">
|
||||
<a
|
||||
|
@ -50,6 +50,13 @@ export class VaultListItemsContainerComponent {
|
||||
@Input()
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Optional description for the vault list item section. Will be shown below the title even when
|
||||
* no ciphers are available.
|
||||
*/
|
||||
@Input()
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Option to show a refresh button in the section header.
|
||||
*/
|
||||
|
@ -1,106 +1,77 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<bit-container>
|
||||
<p>{{ "preferencesDesc" | i18n }}</p>
|
||||
<form [formGroup]="form" (ngSubmit)="submit()" ngNativeValidate>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
|
||||
<span *ngIf="policy.timeout && policy.action">
|
||||
{{
|
||||
"vaultTimeoutPolicyWithActionInEffect"
|
||||
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
|
||||
}}
|
||||
</span>
|
||||
<span *ngIf="policy.timeout && !policy.action">
|
||||
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
|
||||
</span>
|
||||
<span *ngIf="!policy.timeout && policy.action">
|
||||
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
||||
</span>
|
||||
</app-callout>
|
||||
<app-vault-timeout-input
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</app-vault-timeout-input>
|
||||
</div>
|
||||
</div>
|
||||
<p bitTypography="body1">{{ "preferencesDesc" | i18n }}</p>
|
||||
<form [formGroup]="form" [bitSubmit]="submit" class="tw-w-1/2">
|
||||
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
|
||||
<span *ngIf="policy.timeout && policy.action">
|
||||
{{
|
||||
"vaultTimeoutPolicyWithActionInEffect"
|
||||
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
|
||||
}}
|
||||
</span>
|
||||
<span *ngIf="policy.timeout && !policy.action">
|
||||
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
|
||||
</span>
|
||||
<span *ngIf="!policy.timeout && policy.action">
|
||||
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
||||
</span>
|
||||
</app-callout>
|
||||
<app-vault-timeout-input
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</app-vault-timeout-input>
|
||||
<ng-container *ngIf="availableVaultTimeoutActions$ | async as availableVaultTimeoutActions">
|
||||
<div *ngIf="availableVaultTimeoutActions.length > 1" class="form-group">
|
||||
<label>{{ "vaultTimeoutAction" | i18n }}</label>
|
||||
<div
|
||||
<bit-radio-group
|
||||
formControlName="vaultTimeoutAction"
|
||||
*ngIf="availableVaultTimeoutActions.length > 1"
|
||||
>
|
||||
<bit-label>{{ "vaultTimeoutAction" | i18n }}</bit-label>
|
||||
<bit-radio-button
|
||||
*ngIf="availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
|
||||
class="form-check form-check-block"
|
||||
id="vaultTimeoutActionLock"
|
||||
[value]="VaultTimeoutAction.Lock"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="vaultTimeoutAction"
|
||||
id="vaultTimeoutActionLock"
|
||||
value="{{ VaultTimeoutAction.Lock }}"
|
||||
formControlName="vaultTimeoutAction"
|
||||
/>
|
||||
<label class="form-check-label" for="vaultTimeoutActionLock">
|
||||
{{ "lock" | i18n }}
|
||||
<small>{{ "vaultTimeoutActionLockDesc" | i18n }}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
<bit-label>{{ "lock" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "vaultTimeoutActionLockDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
*ngIf="availableVaultTimeoutActions.includes(VaultTimeoutAction.LogOut)"
|
||||
class="form-check mt-2 form-check-block"
|
||||
id="vaultTimeoutActionLogOut"
|
||||
[value]="VaultTimeoutAction.LogOut"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="vaultTimeoutAction"
|
||||
id="vaultTimeoutActionLogOut"
|
||||
value="{{ VaultTimeoutAction.LogOut }}"
|
||||
formControlName="vaultTimeoutAction"
|
||||
/>
|
||||
<label class="form-check-label" for="vaultTimeoutActionLogOut">
|
||||
{{ "logOut" | i18n }}
|
||||
<small>{{ "vaultTimeoutActionLogOutDesc" | i18n }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-label>{{ "logOut" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "vaultTimeoutActionLogOutDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</ng-container>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<div class="d-flex">
|
||||
<label for="locale">{{ "language" | i18n }}</label>
|
||||
<a
|
||||
class="ml-auto"
|
||||
href="https://bitwarden.com/help/localization/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<select id="locale" name="Locale" formControlName="locale" class="form-control">
|
||||
<option *ngFor="let o of localeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ "languageDesc" | i18n }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enableFavicons"
|
||||
name="enableFavicons"
|
||||
formControlName="enableFavicons"
|
||||
/>
|
||||
<label class="form-check-label" for="enableFavicons">
|
||||
{{ "enableFavicon" | i18n }}
|
||||
</label>
|
||||
<bit-form-field>
|
||||
<bit-label
|
||||
>{{ "language" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
class="tw-float-right"
|
||||
href="https://bitwarden.com/help/localization/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
<bit-select formControlName="locale" id="locale">
|
||||
<bit-option *ngFor="let o of localeOptions" [value]="o.value" [label]="o.name"></bit-option>
|
||||
</bit-select>
|
||||
<bit-hint>{{ "languageDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="enableFavicons" />
|
||||
<bit-label
|
||||
>{{ "enableFavicon" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/website-icons/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@ -108,22 +79,16 @@
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{ "faviconDesc" | i18n }}</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="theme">{{ "theme" | i18n }}</label>
|
||||
<select id="theme" name="theme" formControlName="theme" class="form-control">
|
||||
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ "themeDesc" | i18n }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</bit-label>
|
||||
<bit-hint>{{ "faviconDesc" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "theme" | i18n }}</bit-label>
|
||||
<bit-select formControlName="theme" id="theme">
|
||||
<bit-option *ngFor="let o of themeOptions" [value]="o.value" [label]="o.name"></bit-option>
|
||||
</bit-select>
|
||||
<bit-hint>{{ "themeDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
|
||||
</form>
|
||||
</bit-container>
|
||||
|
@ -158,7 +158,7 @@ export class PreferencesComponent implements OnInit {
|
||||
this.form.setValue(initialFormValues, { emitEvent: false });
|
||||
}
|
||||
|
||||
async submit() {
|
||||
submit = async () => {
|
||||
if (!this.form.controls.vaultTimeout.valid) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
@ -188,7 +188,7 @@ export class PreferencesComponent implements OnInit {
|
||||
this.i18nService.t("preferencesUpdated"),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
ProviderSelectPaymentMethodDialogComponent,
|
||||
ProviderSubscriptionComponent,
|
||||
} from "../../billing/providers";
|
||||
import { SubscriptionStatusComponent } from "../../billing/providers/subscription/subscription-status.component";
|
||||
|
||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
@ -70,6 +71,7 @@ import { SetupComponent } from "./setup/setup.component";
|
||||
ProviderSubscriptionComponent,
|
||||
ProviderSelectPaymentMethodDialogComponent,
|
||||
ProviderPaymentMethodComponent,
|
||||
SubscriptionStatusComponent,
|
||||
],
|
||||
providers: [WebProviderService, ProviderPermissionsGuard],
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large">
|
||||
<bit-dialog dialogSize="large" [loading]="loading">
|
||||
<span bitDialogTitle class="tw-font-semibold">
|
||||
{{ "newClientOrganization" | i18n }}
|
||||
</span>
|
||||
@ -49,11 +49,21 @@
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4">
|
||||
<bit-form-field>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>
|
||||
{{ "seats" | i18n }}
|
||||
</bit-label>
|
||||
<input type="text" bitInput formControlName="seats" />
|
||||
<bit-hint
|
||||
class="tw-text-muted tw-grid tw-grid-flow-col tw-gap-1 tw-grid-cols-1 tw-grid-rows-2"
|
||||
*ngIf="unassignedSeatsForSelectedPlan > 0"
|
||||
>
|
||||
<span class="tw-col-span-1"
|
||||
>{{ unassignedSeatsForSelectedPlan }}
|
||||
{{ "unassignedSeatsDescription" | i18n | lowercase }}</span
|
||||
>
|
||||
<span class="tw-col-span-1">0 {{ "purchaseSeatDescription" | i18n | lowercase }}</span>
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,11 +2,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
import { ProviderPlanResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||
|
||||
@ -33,6 +34,7 @@ type PlanCard = {
|
||||
name: string;
|
||||
cost: number;
|
||||
type: PlanType;
|
||||
plan: PlanResponse;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
@ -41,20 +43,24 @@ type PlanCard = {
|
||||
templateUrl: "./create-client-organization.component.html",
|
||||
})
|
||||
export class CreateClientOrganizationComponent implements OnInit {
|
||||
protected ResultType = CreateClientOrganizationResultType;
|
||||
protected formGroup = this.formBuilder.group({
|
||||
clientOwnerEmail: ["", [Validators.required, Validators.email]],
|
||||
organizationName: ["", Validators.required],
|
||||
seats: [null, [Validators.required, Validators.min(1)]],
|
||||
});
|
||||
protected loading = true;
|
||||
protected planCards: PlanCard[];
|
||||
protected ResultType = CreateClientOrganizationResultType;
|
||||
|
||||
private providerPlans: ProviderPlanResponse[];
|
||||
|
||||
constructor(
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
@Inject(DIALOG_DATA) private dialogParams: CreateClientOrganizationParams,
|
||||
private dialogRef: DialogRef<CreateClientOrganizationResultType>,
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
private webProviderService: WebProviderService,
|
||||
) {}
|
||||
|
||||
@ -92,6 +98,11 @@ export class CreateClientOrganizationComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const subscription = await this.billingApiService.getProviderSubscription(
|
||||
this.dialogParams.providerId,
|
||||
);
|
||||
this.providerPlans = subscription?.plans ?? [];
|
||||
|
||||
const teamsPlan = this.dialogParams.plans.find((plan) => plan.type === PlanType.TeamsMonthly);
|
||||
const enterprisePlan = this.dialogParams.plans.find(
|
||||
(plan) => plan.type === PlanType.EnterpriseMonthly,
|
||||
@ -102,15 +113,19 @@ export class CreateClientOrganizationComponent implements OnInit {
|
||||
name: this.i18nService.t("planNameTeams"),
|
||||
cost: teamsPlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
|
||||
type: teamsPlan.type,
|
||||
plan: teamsPlan,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t("planNameEnterprise"),
|
||||
cost: enterprisePlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
|
||||
type: enterprisePlan.type,
|
||||
plan: enterprisePlan,
|
||||
selected: false,
|
||||
},
|
||||
];
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
protected selectPlan(name: string) {
|
||||
@ -135,8 +150,23 @@ export class CreateClientOrganizationComponent implements OnInit {
|
||||
this.formGroup.value.seats,
|
||||
);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("createdNewClient"));
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("createdNewClient"),
|
||||
});
|
||||
|
||||
this.dialogRef.close(this.ResultType.Submitted);
|
||||
};
|
||||
|
||||
protected get unassignedSeatsForSelectedPlan(): number {
|
||||
if (this.loading || !this.planCards) {
|
||||
return 0;
|
||||
}
|
||||
const selectedPlan = this.planCards.find((planCard) => planCard.selected).plan;
|
||||
const selectedProviderPlan = this.providerPlans.find(
|
||||
(providerPlan) => providerPlan.planName === selectedPlan.name,
|
||||
);
|
||||
return selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats;
|
||||
}
|
||||
}
|
||||
|
@ -7,22 +7,20 @@
|
||||
<p>
|
||||
{{ "manageSeatsDescription" | i18n }}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>
|
||||
{{ "assignedSeats" | i18n }}
|
||||
</bit-label>
|
||||
<input id="assignedSeats" type="number" bitInput required [(ngModel)]="assignedSeats" />
|
||||
<bit-hint class="tw-text-muted" *ngIf="remainingOpenSeats > 0">
|
||||
<div class="tw-grid tw-grid-flow-col tw-gap-1 tw-grid-cols-1 tw-grid-rows-2">
|
||||
<span class="tw-col-span-1"
|
||||
>{{ unassignedSeats }} {{ "unassignedSeatsDescription" | i18n | lowercase }}</span
|
||||
>
|
||||
<span class="tw-col-span-1">0 {{ "purchaseSeatDescription" | i18n | lowercase }}</span>
|
||||
</div>
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
<ng-container *ngIf="remainingOpenSeats > 0">
|
||||
<p>
|
||||
<small class="tw-text-muted">{{ unassignedSeats }}</small>
|
||||
<small class="tw-text-muted">{{ "unassignedSeatsDescription" | i18n }}</small>
|
||||
</p>
|
||||
<p>
|
||||
<small class="tw-text-muted">{{ AdditionalSeatPurchased }}</small>
|
||||
<small class="tw-text-muted">{{ "purchaseSeatDescription" | i18n }}</small>
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
|
@ -4,7 +4,7 @@ import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||
import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request";
|
||||
import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
import { ProviderPlanResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
@ -83,7 +83,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
getPurchasedSeatsByPlan(planName: string, plans: Plans[]): number {
|
||||
getPurchasedSeatsByPlan(planName: string, plans: ProviderPlanResponse[]): number {
|
||||
const plan = plans.find((plan) => plan.planName === planName);
|
||||
if (plan) {
|
||||
return plan.purchasedSeats;
|
||||
@ -92,7 +92,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
getAssignedByPlan(planName: string, plans: Plans[]): number {
|
||||
getAssignedByPlan(planName: string, plans: ProviderPlanResponse[]): number {
|
||||
const plan = plans.find((plan) => plan.planName === planName);
|
||||
if (plan) {
|
||||
return plan.assignedSeats;
|
||||
@ -101,7 +101,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
getProviderSeatMinimumByPlan(planName: string, plans: Plans[]) {
|
||||
getProviderSeatMinimumByPlan(planName: string, plans: ProviderPlanResponse[]) {
|
||||
const plan = plans.find((plan) => plan.planName === planName);
|
||||
if (plan) {
|
||||
return plan.seatMinimum;
|
||||
|
@ -158,6 +158,4 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
||||
|
||||
await this.load();
|
||||
};
|
||||
protected readonly openManageClientOrganizationNameDialog =
|
||||
openManageClientOrganizationNameDialog;
|
||||
}
|
||||
|
@ -1,32 +1,10 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<bit-container>
|
||||
<ng-container *ngIf="!firstLoaded && loading">
|
||||
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="subscription && firstLoaded">
|
||||
<bit-callout type="warning" title="{{ 'canceled' | i18n }}" *ngIf="false">
|
||||
{{ "subscriptionCanceled" | i18n }}</bit-callout
|
||||
>
|
||||
|
||||
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
|
||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||
<dd>{{ "providerPlan" | i18n }}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{ "status" | i18n }}</dt>
|
||||
<dd>
|
||||
<span class="tw-capitalize">{{ subscription.status }}</span>
|
||||
</dd>
|
||||
<dt [ngClass]="{ 'tw-text-danger': isExpired }">{{ "nextCharge" | i18n }}</dt>
|
||||
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ subscription.currentPeriodEndDate | date: "mediumDate" }}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</ng-container>
|
||||
|
||||
<app-subscription-status [providerSubscriptionResponse]="subscription"> </app-subscription-status>
|
||||
<ng-container>
|
||||
<div class="tw-flex-col">
|
||||
<strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 pb-2"
|
||||
|
@ -4,7 +4,7 @@ import { Subject, concatMap, takeUntil } from "rxjs";
|
||||
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||
import {
|
||||
Plans,
|
||||
ProviderPlanResponse,
|
||||
ProviderSubscriptionResponse,
|
||||
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
|
||||
@ -75,7 +75,7 @@ export class ProviderSubscriptionComponent {
|
||||
return totalSeats > 1 ? totalSeats.toString() : "";
|
||||
}
|
||||
|
||||
sumCost(plans: Plans[]): number {
|
||||
sumCost(plans: ProviderPlanResponse[]): number {
|
||||
return plans.reduce((acc, plan) => acc + plan.cost, 0);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
<ng-container>
|
||||
<bit-callout *ngIf="data.callout" [type]="data.callout.severity" [title]="data.callout.header">
|
||||
<p>{{ data.callout.body }}</p>
|
||||
<button
|
||||
*ngIf="data.callout.showReinstatementButton"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[bitAction]="requestReinstatement"
|
||||
type="button"
|
||||
>
|
||||
{{ "reinstateSubscription" | i18n }}
|
||||
</button>
|
||||
</bit-callout>
|
||||
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
|
||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||
<dd>{{ "providerPlan" | i18n }}</dd>
|
||||
<ng-container *ngIf="data.status && data.date">
|
||||
<dt>{{ data.status.label }}</dt>
|
||||
<dd>
|
||||
<span class="tw-capitalize">
|
||||
{{ displayedStatus }}
|
||||
</span>
|
||||
</dd>
|
||||
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ data.date.label | titlecase }}
|
||||
</dt>
|
||||
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ data.date.value | date: "mediumDate" }}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</ng-container>
|
@ -0,0 +1,188 @@
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ProviderSubscriptionResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
type ComponentData = {
|
||||
status?: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
date?: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
callout?: {
|
||||
severity: "danger" | "warning";
|
||||
header: string;
|
||||
body: string;
|
||||
showReinstatementButton: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-subscription-status",
|
||||
templateUrl: "subscription-status.component.html",
|
||||
})
|
||||
export class SubscriptionStatusComponent {
|
||||
@Input({ required: true }) providerSubscriptionResponse: ProviderSubscriptionResponse;
|
||||
@Output() reinstatementRequested = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private datePipe: DatePipe,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
get displayedStatus(): string {
|
||||
return this.data.status.value;
|
||||
}
|
||||
|
||||
get planName() {
|
||||
return this.providerSubscriptionResponse.plans[0];
|
||||
}
|
||||
|
||||
get status(): string {
|
||||
return this.subscription.status;
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.subscription.status !== "active";
|
||||
}
|
||||
|
||||
get subscription() {
|
||||
return this.providerSubscriptionResponse;
|
||||
}
|
||||
|
||||
get data(): ComponentData {
|
||||
const defaultStatusLabel = this.i18nService.t("status");
|
||||
|
||||
const nextChargeDateLabel = this.i18nService.t("nextCharge");
|
||||
const subscriptionExpiredDateLabel = this.i18nService.t("subscriptionExpired");
|
||||
const cancellationDateLabel = this.i18nService.t("cancellationDate");
|
||||
|
||||
switch (this.status) {
|
||||
case "free": {
|
||||
return {};
|
||||
}
|
||||
case "trialing": {
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: this.i18nService.t("trial"),
|
||||
},
|
||||
date: {
|
||||
label: nextChargeDateLabel,
|
||||
value: this.subscription.currentPeriodEndDate.toDateString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
case "active": {
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: this.i18nService.t("active"),
|
||||
},
|
||||
date: {
|
||||
label: nextChargeDateLabel,
|
||||
value: this.subscription.currentPeriodEndDate.toDateString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
case "past_due": {
|
||||
const pastDueText = this.i18nService.t("pastDue");
|
||||
const suspensionDate = this.datePipe.transform(
|
||||
this.subscription.suspensionDate,
|
||||
"mediumDate",
|
||||
);
|
||||
const calloutBody =
|
||||
this.subscription.collectionMethod === "charge_automatically"
|
||||
? this.i18nService.t(
|
||||
"pastDueWarningForChargeAutomatically",
|
||||
this.subscription.gracePeriod,
|
||||
suspensionDate,
|
||||
)
|
||||
: this.i18nService.t(
|
||||
"pastDueWarningForSendInvoice",
|
||||
this.subscription.gracePeriod,
|
||||
suspensionDate,
|
||||
);
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: pastDueText,
|
||||
},
|
||||
date: {
|
||||
label: subscriptionExpiredDateLabel,
|
||||
value: this.subscription.unpaidPeriodEndDate,
|
||||
},
|
||||
callout: {
|
||||
severity: "warning",
|
||||
header: pastDueText,
|
||||
body: calloutBody,
|
||||
showReinstatementButton: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "unpaid": {
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: this.i18nService.t("unpaid"),
|
||||
},
|
||||
date: {
|
||||
label: subscriptionExpiredDateLabel,
|
||||
value: this.subscription.currentPeriodEndDate.toDateString(),
|
||||
},
|
||||
callout: {
|
||||
severity: "danger",
|
||||
header: this.i18nService.t("unpaidInvoice"),
|
||||
body: this.i18nService.t("toReactivateYourSubscription"),
|
||||
showReinstatementButton: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "pending_cancellation": {
|
||||
const pendingCancellationText = this.i18nService.t("pendingCancellation");
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: pendingCancellationText,
|
||||
},
|
||||
date: {
|
||||
label: cancellationDateLabel,
|
||||
value: this.subscription.currentPeriodEndDate.toDateString(),
|
||||
},
|
||||
callout: {
|
||||
severity: "warning",
|
||||
header: pendingCancellationText,
|
||||
body: this.i18nService.t("subscriptionPendingCanceled"),
|
||||
showReinstatementButton: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "incomplete_expired":
|
||||
case "canceled": {
|
||||
const canceledText = this.i18nService.t("canceled");
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: canceledText,
|
||||
},
|
||||
date: {
|
||||
label: cancellationDateLabel,
|
||||
value: this.subscription.currentPeriodEndDate.toDateString(),
|
||||
},
|
||||
callout: {
|
||||
severity: "danger",
|
||||
header: canceledText,
|
||||
body: this.i18nService.t("subscriptionCanceled"),
|
||||
showReinstatementButton: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestReinstatement = () => this.reinstatementRequested.emit();
|
||||
}
|
@ -4,21 +4,29 @@ export class ProviderSubscriptionResponse extends BaseResponse {
|
||||
status: string;
|
||||
currentPeriodEndDate: Date;
|
||||
discountPercentage?: number | null;
|
||||
plans: Plans[] = [];
|
||||
plans: ProviderPlanResponse[] = [];
|
||||
collectionMethod: string;
|
||||
unpaidPeriodEndDate?: string;
|
||||
gracePeriod?: number | null;
|
||||
suspensionDate?: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.status = this.getResponseProperty("status");
|
||||
this.currentPeriodEndDate = new Date(this.getResponseProperty("currentPeriodEndDate"));
|
||||
this.discountPercentage = this.getResponseProperty("discountPercentage");
|
||||
this.collectionMethod = this.getResponseProperty("collectionMethod");
|
||||
this.unpaidPeriodEndDate = this.getResponseProperty("unpaidPeriodEndDate");
|
||||
this.gracePeriod = this.getResponseProperty("gracePeriod");
|
||||
this.suspensionDate = this.getResponseProperty("suspensionDate");
|
||||
const plans = this.getResponseProperty("plans");
|
||||
if (plans != null) {
|
||||
this.plans = plans.map((i: any) => new Plans(i));
|
||||
this.plans = plans.map((i: any) => new ProviderPlanResponse(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Plans extends BaseResponse {
|
||||
export class ProviderPlanResponse extends BaseResponse {
|
||||
planName: string;
|
||||
seatMinimum: number;
|
||||
assignedSeats: number;
|
||||
|
Loading…
Reference in New Issue
Block a user