1
0
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:
gbubemismith 2024-06-06 13:59:17 -04:00
commit cf802fc32c
No known key found for this signature in database
17 changed files with 391 additions and 187 deletions

View File

@ -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>

View File

@ -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) {

View File

@ -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

View File

@ -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.
*/

View File

@ -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>

View File

@ -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();

View File

@ -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],
})

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;

View File

@ -158,6 +158,4 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
await this.load();
};
protected readonly openManageClientOrganizationNameDialog =
openManageClientOrganizationNameDialog;
}

View File

@ -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"

View File

@ -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);
}

View File

@ -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>

View File

@ -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();
}

View File

@ -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;