1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 03:03:43 +00:00

[AC-1011] Admin Console / Billing code ownership (#4973)

* refactor: move SCIM component to admin-console, refs EC-1011

* refactor: move scimProviderType to admin-console, refs EC-1011

* refactor: move scim-config.api to admin-console, refs EC-1011

* refactor: create models folder and nest existing api contents, refs EC-1011

* refactor: move scim-config to admin-console models, refs EC-1011

* refactor: move billing.component to billing, refs EC-1011

* refactor: remove nested app folder from new billing structure, refs EC-1011

* refactor: move organizations/billing to billing, refs EC-1011

* refactor: move add-credit and adjust-payment to billing/settings, refs EC-1011

* refactor: billing history/sync to billing, refs EC-1011

* refactor: move org plans, payment/method to billing/settings, refs EC-1011

* fix: update legacy file paths for payment-method and tax-info, refs EC-1011

* fix: update imports for scim component, refs EC-1011

* refactor: move subscription and tax-info into billing, refs EC-1011

* refactor: move user-subscription to billing, refs EC-1011

* refactor: move images/cards to billing and update base path, refs EC-1011

* refactor: move payment-method, plan subscription, and plan to billing, refs EC-1011

* refactor: move transaction-type to billing, refs EC-1011

* refactor: move billing-sync-config to billing, refs EC-1011

* refactor: move billing-sync and bit-pay-invoice request to billing, refs EC-1011

* refactor: move org subscription and tax info update requests to billing, refs EC-1011

* fix: broken paths to billing, refs EC-1011

* refactor: move payment request to billing, refs EC-1011

* fix: update remaining imports for payment-request, refs EC-1011

* refactor: move tax-info-update to billing, refs EC-1011

* refactor: move billing-payment, billing-history, and billing responses to billing, refs EC-1011

* refactor: move organization-subscription-responset to billing, refs EC-1011

* refactor: move payment and plan responses to billing, refs EC-1011

* refactor: move subscription response to billing ,refs EC-1011

* refactor: move tax info and rate responses to billing, refs EC-1011

* fix: update remaining path to base response for tax-rate response, refs EC-1011

* refactor: (browser) move organization-service to admin-console, refs EC-1011

* refactor: (browser) move organizaiton-service to admin-console, refs EC-1011

* refactor: (cli) move share command to admin-console, refs EC-1011

* refactor: move organization-collect request model to admin-console, refs EC-1011

* refactor: (web) move organization, collection/user responses to admin-console, refs EC-1011

* refactor: (cli) move selection-read-only to admin-console, refs EC-1011

* refactor: (desktop) move organization-filter to admin-console, refs EC-1011

* refactor: (web) move organization-switcher to admin-console, refs EC-1011

* refactor: (web) move access-selector to admin-console, refs EC-1011

* refactor: (web) move create folder to admin-console, refs EC-1011

* refactor: (web) move org guards folder to admin-console, refs EC-1011

* refactor: (web) move org layout to admin-console, refs EC-1011

* refactor: move manage collections to admin console, refs EC-1011

* refactor: (web) move collection-dialog to admin-console, refs EC-1011

* refactor: (web) move entity users/events and events component to admin-console, refs EC-1011

* refactor: (web) move groups/group-add-edit to admin-console, refs EC-1011

* refactor: (web) move manage, org-manage module, and user-confirm to admin-console, refs EC-1011

* refactor: (web) move people to admin-console, refs EC-1011

* refactor: (web) move reset-password to admin-console, refs EC-1011

* refactor: (web) move organization-routing and module to admin-console, refs EC-1011

* refactor: move admin-console and billing within app scope, refs EC-1011

* fix: update leftover merge conflicts, refs EC-1011

* refactor: (web) member-dialog to admin-console, refs EC-1011

* refactor: (web) move policies to admin-console, refs EC-1011

* refactor: (web) move reporting to admin-console, refs EC-1011

* refactor: (web) move settings to admin-console, refs EC-1011

* refactor: (web) move sponsorships to admin-console, refs EC-1011

* refactor: (web) move tools to admin-console, refs EC-1011

* refactor: (web) move users to admin-console, refs EC-1011

* refactor: (web) move collections to admin-console, refs EC-1011

* refactor: (web) move create-organization to admin-console, refs EC-1011

* refactor: (web) move licensed components to admin-console, refs EC-1011

* refactor: (web) move bit organization modules to admin-console, refs EC-1011

* fix: update leftover import statements for organizations.module, refs EC-1011

* refactor: (web) move personal vault and max timeout to admin-console, refs EC-1011

* refactor: (web) move providers to admin-console, refs EC-1011

* refactor: (libs) move organization service to admin-console, refs EC-1011

* refactor: (libs) move profile org/provider responses and other misc org responses to admin-console, refs EC-1011

* refactor: (libs) move provider request and selectionion-read-only request to admin-console, refs EC-1011

* fix: update missed import path for provider-user-update request, refs EC-1011

* refactor: (libs) move abstractions to admin-console, refs EC-1011

* refactor: (libs) move org/provider enums to admin-console, refs EC-1011

* fix: update downstream import statements from libs changes, refs EC-1011

* refactor: (libs) move data files to admin-console, refs EC-1011

* refactor: (libs) move domain to admin-console, refs EC-1011

* refactor: (libs) move request objects to admin-console, refs EC-1011

* fix: update downstream import changes from libs, refs EC-1011

* refactor: move leftover provider files to admin-console, refs EC-1011

* refactor: (browser) move group policy environment to admin-console, refs EC-1011

* fix: (browser) update downstream import statements, refs EC-1011

* fix: (desktop) update downstream libs moves, refs EC-1011

* fix: (cli) update downstream import changes from libs, refs EC-1011

* refactor: move org-auth related files to admin-console, refs EC-1011

* refactor: (libs) move request objects to admin-console, refs EC-1011

* refactor: move persmissions to admin-console, refs EC-1011

* refactor: move sponsored families to admin-console and fix libs changes, refs EC-1011

* refactor: move collections to admin-console, refs EC-1011

* refactor: move spec file back to spec scope, refs EC-1011

* fix: update downstream imports due to libs changes, refs EC-1011

* fix: udpate downstream import changes due to libs, refs EC-1011

* fix: update downstream imports due to libs changes, refs EC-1011

* fix: update downstream imports from libs changes, refs EC-1011

* fix: update path malformation in jslib-services.module, refs EC-1011

* fix: lint errors from improper casing, refs AC-1011

* fix: update downstream filename changes, refs AC-1011

* fix: (cli) update downstream filename changes, refs AC-1011

* fix: (desktop) update downstream filename changes, refs AC-1011

* fix: (browser) update downstream filename changes, refs AC-1011

* fix: lint errors, refs AC-1011

* fix: prettier, refs AC-1011

* fix: lint fixes for import order, refs AC-1011

* fix: update import path for provider user type, refs AC-1011

* fix: update new codes import paths for admin console structure, refs AC-1011

* fix: lint/prettier, refs AC-1011

* fix: update layout stories path, refs AC-1011

* fix: update comoponents card icons base variable in styles, refs AC-1011

* fix: update provider service path in permissions guard spec, refs AC-1011

* fix: update provider permission guard path, refs AC-1011

* fix: remove unecessary TODO for shared index export statement, refs AC-1011

* refactor: move browser-organization service and cli organization-user response out of admin-console, refs AC-1011

* refactor: move web/browser/desktop collections component to vault domain, refs AC-1011

* refactor: move organization.module out of admin-console scope, refs AC-1011

* fix: prettier, refs AC-1011

* refactor: move organizations-api-key.request out of admin-console scope, refs AC-1011
This commit is contained in:
Vincent Salucci
2023-03-22 10:03:50 -05:00
committed by GitHub
parent a7fea2ff3a
commit 780a563ce0
557 changed files with 1260 additions and 1246 deletions

View File

@@ -1,80 +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">&times;</span>
</button>
<h3 class="card-body-header">{{ "addCredit" | i18n }}</h3>
<div class="mb-4 text-lg" *ngIf="showOptions">
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="Method"
id="credit-method-paypal"
[value]="paymentMethodType.PayPal"
[(ngModel)]="method"
/>
<label class="form-check-label" for="credit-method-paypal">
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i> PayPal</label
>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="Method"
id="credit-method-bitcoin"
[value]="paymentMethodType.BitPay"
[(ngModel)]="method"
/>
<label class="form-check-label" for="credit-method-bitcoin">
<i class="bwi bwi-fw bwi-bitcoin" aria-hidden="true"></i> Bitcoin</label
>
</div>
</div>
<div class="form-group">
<div class="row">
<div class="col-4">
<label for="creditAmount">{{ "amount" | i18n }}</label>
<div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">$USD</span></div>
<input
id="creditAmount"
class="form-control"
type="text"
name="CreditAmount"
[(ngModel)]="creditAmount"
(blur)="formatAmount()"
required
/>
</div>
</div>
</div>
<small class="form-text text-muted">{{ "creditDelayed" | i18n }}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading || ppLoading">
<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>
<form #ppButtonForm action="{{ ppButtonFormAction }}" method="post" target="_top">
<input type="hidden" name="cmd" value="_xclick" />
<input type="hidden" name="business" value="{{ ppButtonBusinessId }}" />
<input type="hidden" name="button_subtype" value="services" />
<input type="hidden" name="no_note" value="1" />
<input type="hidden" name="no_shipping" value="1" />
<input type="hidden" name="rm" value="1" />
<input type="hidden" name="return" value="{{ returnUrl }}" />
<input type="hidden" name="cancel_return" value="{{ returnUrl }}" />
<input type="hidden" name="currency_code" value="USD" />
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
<input type="hidden" name="amount" value="{{ creditAmount }}" />
<input type="hidden" name="custom" value="{{ ppButtonCustomField }}" />
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
<input type="hidden" name="item_number" value="{{ subject }}" />
</form>

View File

@@ -1,149 +0,0 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PayPalConfig } from "@bitwarden/common/abstractions/environment.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
import { BitPayInvoiceRequest } from "@bitwarden/common/models/request/bit-pay-invoice.request";
@Component({
selector: "app-add-credit",
templateUrl: "add-credit.component.html",
})
export class AddCreditComponent implements OnInit {
@Input() creditAmount: string;
@Input() showOptions = true;
@Input() method = PaymentMethodType.PayPal;
@Input() organizationId: string;
@Output() onAdded = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
paymentMethodType = PaymentMethodType;
ppButtonFormAction: string;
ppButtonBusinessId: string;
ppButtonCustomField: string;
ppLoading = false;
subject: string;
returnUrl: string;
formPromise: Promise<any>;
private userId: string;
private name: string;
private email: string;
constructor(
private stateService: StateService,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private logService: LogService
) {
const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
this.ppButtonFormAction = payPalConfig.buttonAction;
this.ppButtonBusinessId = payPalConfig.businessId;
}
async ngOnInit() {
if (this.organizationId != null) {
if (this.creditAmount == null) {
this.creditAmount = "20.00";
}
this.ppButtonCustomField = "organization_id:" + this.organizationId;
const org = await this.organizationService.get(this.organizationId);
if (org != null) {
this.subject = org.name;
this.name = org.name;
}
} else {
if (this.creditAmount == null) {
this.creditAmount = "10.00";
}
this.userId = await this.stateService.getUserId();
this.subject = await this.stateService.getEmail();
this.email = this.subject;
this.ppButtonCustomField = "user_id:" + this.userId;
}
this.ppButtonCustomField += ",account_credit:1";
this.returnUrl = window.location.href;
}
async submit() {
if (this.creditAmount == null || this.creditAmount === "") {
return;
}
if (this.method === PaymentMethodType.PayPal) {
this.ppButtonFormRef.nativeElement.submit();
this.ppLoading = true;
return;
}
if (this.method === PaymentMethodType.BitPay) {
try {
const req = new BitPayInvoiceRequest();
req.email = this.email;
req.name = this.name;
req.credit = true;
req.amount = this.creditAmountNumber;
req.organizationId = this.organizationId;
req.userId = this.userId;
req.returnUrl = this.returnUrl;
this.formPromise = this.apiService.postBitPayInvoice(req);
const bitPayUrl: string = await this.formPromise;
this.platformUtilsService.launchUri(bitPayUrl);
} catch (e) {
this.logService.error(e);
}
return;
}
try {
this.onAdded.emit();
} catch (e) {
this.logService.error(e);
}
}
cancel() {
this.onCanceled.emit();
}
formatAmount() {
try {
if (this.creditAmount != null && this.creditAmount !== "") {
const floatAmount = Math.abs(parseFloat(this.creditAmount));
if (floatAmount > 0) {
this.creditAmount = parseFloat((Math.round(floatAmount * 100) / 100).toString())
.toFixed(2)
.toString();
return;
}
}
} catch (e) {
this.logService.error(e);
}
this.creditAmount = "";
}
get creditAmountNumber(): number {
if (this.creditAmount != null && this.creditAmount !== "") {
try {
return parseFloat(this.creditAmount);
} catch (e) {
this.logService.error(e);
}
}
return null;
}
}

View File

@@ -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">&times;</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>

View File

@@ -1,85 +0,0 @@
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
import { PaymentRequest } from "@bitwarden/common/models/request/payment.request";
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
) {}
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;
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();
}
}
}
}

View File

@@ -4,12 +4,12 @@ import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PaymentResponse } from "@bitwarden/common/billing/models/response/payment.response";
import { StorageRequest } from "@bitwarden/common/models/request/storage.request";
import { PaymentResponse } from "@bitwarden/common/models/response/payment.response";
import { PaymentComponent } from "./payment.component";
import { PaymentComponent } from "../billing/settings/payment.component";
@Component({
selector: "app-adjust-storage",

View File

@@ -1,29 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="d-flex tabbed-header">
<h1>
{{ "billingHistory" | i18n }}
</h1>
<button
bitButton
buttonType="secondary"
(click)="load()"
class="tw-ml-auto"
*ngIf="firstLoaded"
[disabled]="loading"
>
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
{{ "refresh" | i18n }}
</button>
</div>
<ng-container *ngIf="!firstLoaded && 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>
<ng-container *ngIf="billing">
<app-billing-history [billing]="billing"></app-billing-history>
</ng-container>

View File

@@ -1,40 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { BillingHistoryResponse } from "@bitwarden/common/models/response/billing-history.response";
@Component({
selector: "app-billing-history-view",
templateUrl: "billing-history-view.component.html",
})
export class BillingHistoryViewComponent implements OnInit {
loading = false;
firstLoaded = false;
billing: BillingHistoryResponse;
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private router: Router
) {}
async ngOnInit() {
if (this.platformUtilsService.isSelfHost()) {
this.router.navigate(["/settings/subscription"]);
return;
}
await this.load();
this.firstLoaded = true;
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
this.billing = await this.apiService.getUserBillingHistory();
this.loading = false;
}
}

View File

@@ -1,65 +0,0 @@
<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>
<a
href="{{ i.pdfUrl }}"
target="_blank"
rel="noopener"
class="mr-2"
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
>
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
></a>
<a href="{{ i.url }}" target="_blank" rel="noopener" title="{{ 'viewInvoice' | i18n }}">
{{ "invoiceNumber" | i18n : i.number }}</a
>
</td>
<td>{{ i.amount | currency : "$" }}</td>
<td>
<span *ngIf="i.paid">
<i class="bwi bwi-check 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>
{{ "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>
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
{{ "chargeNoun" | i18n }}
</span>
<span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span>
</td>
<td>
<i
class="bwi bwi-fw"
*ngIf="t.paymentMethodType"
aria-hidden="true"
[ngClass]="paymentMethodClasses(t.paymentMethodType)"
></i>
{{ t.details }}
</td>
<td
[ngClass]="{ 'text-strike': t.refunded }"
title="{{ (t.refunded ? 'refunded' : '') | i18n }}"
>
{{ t.amount | currency : "$" }}
</td>
</tr>
</tbody>
</table>
<small class="text-muted">* {{ "chargesStatement" | i18n : "BITWARDEN" }}</small>

View File

@@ -1,41 +0,0 @@
import { Component, Input } from "@angular/core";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
import { TransactionType } from "@bitwarden/common/enums/transactionType";
import { BillingHistoryResponse } from "@bitwarden/common/models/response/billing-history.response";
@Component({
selector: "app-billing-history",
templateUrl: "billing-history.component.html",
})
export class BillingHistoryComponent {
@Input()
billing: BillingHistoryResponse;
paymentMethodType = PaymentMethodType;
transactionType = TransactionType;
get invoices() {
return this.billing != null ? this.billing.invoices : null;
}
get transactions() {
return this.billing != null ? this.billing.transactions : null;
}
paymentMethodClasses(type: PaymentMethodType) {
switch (type) {
case PaymentMethodType.Card:
return ["bwi-credit-card"];
case PaymentMethodType.BankAccount:
case PaymentMethodType.WireTransfer:
return ["bwi-bank"];
case PaymentMethodType.BitPay:
return ["bwi-bitcoin text-warning"];
case PaymentMethodType.PayPal:
return ["bwi-paypal text-primary"];
default:
return [];
}
}
}

View File

@@ -1,69 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h1 class="modal-title" id="billingSyncTitle">{{ "manageBillingSync" | i18n }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
<div class="form-group">
<label for="billingSyncKey"
>{{ "billingSyncKey" | i18n }} <small>(</small><small>{{ "required" | i18n }}</small
><small>)</small></label
>
<input
id="billingSyncKey"
type="input"
name="billingSyncKey"
class="form-control"
[(ngModel)]="billingSyncKey"
required
appAutofocus
appInputVerbatim
/>
</div>
</div>
<div class="modal-footer">
<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>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
type="button"
(click)="deleteConnection()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
[disabled]="form.loading"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="form.loading" aria-hidden="true"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -1,81 +0,0 @@
import { Component } from "@angular/core";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalConfig } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationConnectionType } from "@bitwarden/common/enums/organizationConnectionType";
import { BillingSyncConfigApi } from "@bitwarden/common/models/api/billing-sync-config.api";
import { BillingSyncConfigRequest } from "@bitwarden/common/models/request/billing-sync-config.request";
import { OrganizationConnectionRequest } from "@bitwarden/common/models/request/organization-connection.request";
import { OrganizationConnectionResponse } from "@bitwarden/common/models/response/organization-connection.response";
export interface BillingSyncKeyModalData {
entityId: string;
existingConnectionId: string;
billingSyncKey: string;
setParentConnection: (connection: OrganizationConnectionResponse<BillingSyncConfigApi>) => void;
}
@Component({
selector: "app-billing-sync-key",
templateUrl: "billing-sync-key.component.html",
})
export class BillingSyncKeyComponent {
entityId: string;
existingConnectionId: string;
billingSyncKey: string;
setParentConnection: (connection: OrganizationConnectionResponse<BillingSyncConfigApi>) => void;
formPromise: Promise<OrganizationConnectionResponse<BillingSyncConfigApi>> | Promise<void>;
constructor(
private apiService: ApiService,
private logService: LogService,
protected modalRef: ModalRef,
config: ModalConfig<BillingSyncKeyModalData>
) {
this.entityId = config.data.entityId;
this.existingConnectionId = config.data.existingConnectionId;
this.billingSyncKey = config.data.billingSyncKey;
this.setParentConnection = config.data.setParentConnection;
}
async submit() {
try {
const request = new OrganizationConnectionRequest(
this.entityId,
OrganizationConnectionType.CloudBillingSync,
true,
new BillingSyncConfigRequest(this.billingSyncKey)
);
if (this.existingConnectionId == null) {
this.formPromise = this.apiService.createOrganizationConnection(
request,
BillingSyncConfigApi
);
} else {
this.formPromise = this.apiService.updateOrganizationConnection(
request,
BillingSyncConfigApi,
this.existingConnectionId
);
}
const response = (await this
.formPromise) as OrganizationConnectionResponse<BillingSyncConfigApi>;
this.existingConnectionId = response?.id;
this.billingSyncKey = response?.config?.billingSyncKey;
this.setParentConnection(response);
this.modalRef.close();
} catch (e) {
this.logService.error(e);
}
}
async deleteConnection() {
this.formPromise = this.apiService.deleteOrganizationConnection(this.existingConnectionId);
await this.formPromise;
this.setParentConnection(null);
this.modalRef.close();
}
}

View File

@@ -10,12 +10,12 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { SendService } from "@bitwarden/common/abstractions/send.service";
import { StateService } from "@bitwarden/common/abstractions/state.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 { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";

View File

@@ -1,11 +0,0 @@
<div class="container page-content">
<div class="row">
<div class="col-12">
<div class="page-header">
<h1>{{ "newOrganization" | i18n }}</h1>
</div>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
</div>
</div>
</div>

View File

@@ -1,36 +0,0 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { PlanType } from "@bitwarden/common/enums/planType";
import { ProductType } from "@bitwarden/common/enums/productType";
import { OrganizationPlansComponent } from "./organization-plans.component";
@Component({
selector: "app-create-organization",
templateUrl: "create-organization.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: true })
orgPlansComponent: OrganizationPlansComponent;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.plan === "families") {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
this.orgPlansComponent.product = ProductType.Families;
} else if (qParams.plan === "teams") {
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
this.orgPlansComponent.product = ProductType.Teams;
} else if (qParams.plan === "enterprise") {
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
this.orgPlansComponent.product = ProductType.Enterprise;
}
});
}
}

View File

@@ -1,316 +0,0 @@
<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>
<ng-container *ngIf="createOrganization && selfHosted">
<p>{{ "uploadLicenseFileOrg" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="form-group">
<label for="file">{{ "licenseFile" | i18n }}</label>
<input type="file" id="file" class="form-control-file" name="file" required />
<small class="form-text text-muted">{{
"licenseFileDesc" | i18n : "bitwarden_organization_license.json"
}}</small>
</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>
</button>
</form>
</ng-container>
<form
#form
[formGroup]="formGroup"
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="!loading && !selfHosted && this.plans"
class="tw-pt-6"
>
<app-org-info
(changedBusinessOwned)="changedOwnedBusiness()"
[formGroup]="formGroup"
[createOrganization]="createOrganization"
[isProvider]="!!providerId"
[acceptingSponsorship]="acceptingSponsorship"
></app-org-info>
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2>
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
name="product"
id="product{{ selectableProduct.product }}"
[value]="selectableProduct.product"
formControlName="product"
(change)="changedProduct()"
/>
<label class="form-check-label" for="product{{ selectableProduct.product }}">
{{ selectableProduct.nameLocalizationKey | i18n }}
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n : "1" }}</small>
<ng-container
*ngIf="selectableProduct.product === productTypes.Enterprise; else fullFeatureList"
>
<small>• {{ "includeAllTeamsFeatures" | i18n }}</small>
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small>
<small *ngIf="selectableProduct.hasSso">• {{ "includeSsoAuthentication" | i18n }}</small>
<small *ngIf="selectableProduct.hasPolicies"
>• {{ "includeEnterprisePolicies" | i18n }}</small
>
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
>
{{ "xDayFreeTrial" | i18n : selectableProduct.trialPeriodDays }}
</small>
</ng-container>
<ng-template #fullFeatureList>
<small *ngIf="selectableProduct.product == productTypes.Free"
>• {{ "limitedUsers" | i18n : selectableProduct.maxUsers }}</small
>
<small *ngIf="selectableProduct.product != productTypes.Free && selectableProduct.maxUsers"
>• {{ "addShareLimitedUsers" | i18n : selectableProduct.maxUsers }}</small
>
<small *ngIf="!selectableProduct.maxUsers">• {{ "addShareUnlimitedUsers" | i18n }}</small>
<small *ngIf="selectableProduct.maxCollections"
>• {{ "limitedCollections" | i18n : selectableProduct.maxCollections }}</small
>
<small *ngIf="selectableProduct.maxAdditionalSeats"
>• {{ "addShareLimitedUsers" | i18n : selectableProduct.maxAdditionalSeats }}</small
>
<small *ngIf="!selectableProduct.maxCollections"
>• {{ "createUnlimitedCollections" | i18n }}</small
>
<small *ngIf="selectableProduct.baseStorageGb"
>• {{ "gbEncryptedFileStorage" | i18n : selectableProduct.baseStorageGb + "GB" }}</small
>
<small *ngIf="selectableProduct.hasGroups">• {{ "controlAccessWithGroups" | i18n }}</small>
<small *ngIf="selectableProduct.hasApi">• {{ "trackAuditLogs" | i18n }}</small>
<small *ngIf="selectableProduct.hasDirectory"
>• {{ "syncUsersFromDirectory" | i18n }}</small
>
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small>
<small *ngIf="selectableProduct.usersGetPremium">• {{ "usersGetPremium" | i18n }}</small>
<small *ngIf="selectableProduct.product != productTypes.Free"
>• {{ "priorityCustomerSupport" | i18n }}</small
>
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
>
{{ "xDayFreeTrial" | i18n : selectableProduct.trialPeriodDays }}
</small>
</ng-template>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.basePrice && !acceptingSponsorship">
{{ selectableProduct.basePrice / 12 | currency : "$" }} /{{ "month" | i18n }},
{{ "includesXUsers" | i18n : selectableProduct.baseSeats }}
<ng-container *ngIf="selectableProduct.hasAdditionalSeatsOption">
{{ ("additionalUsers" | i18n).toLowerCase() }}
{{ selectableProduct.seatPrice / 12 | currency : "$" }} /{{ "month" | i18n }}
</ng-container>
</ng-container>
</span>
<span *ngIf="!selectableProduct.basePrice && selectableProduct.hasAdditionalSeatsOption">
{{ "costPerUser" | i18n : (selectableProduct.seatPrice / 12 | currency : "$") }} /{{
"month" | i18n
}}
</span>
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
</label>
</div>
<div *ngIf="formGroup.controls['product'].value !== productTypes.Free">
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
<h2 class="mt-5">{{ "users" | i18n }}</h2>
<div class="row">
<div class="col-6">
<label for="additionalSeats">{{ "userSeats" | i18n }}</label>
<input
id="additionalSeats"
class="form-control"
type="number"
name="additionalSeats"
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
required
/>
<small class="text-muted form-text">{{ "userSeatsHowManyDesc" | i18n }}</small>
</div>
</div>
</ng-container>
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
<div class="row" *ngIf="selectedPlan.hasAdditionalSeatsOption && selectedPlan.baseSeats">
<div class="form-group col-6">
<label for="additionalSeats">{{ "additionalUserSeats" | i18n }}</label>
<input
id="additionalSeats"
class="form-control"
type="number"
name="additionalSeats"
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
"userSeatsAdditionalDesc"
| i18n : selectedPlan.baseSeats : (seatPriceMonthly(selectedPlan) | currency : "$")
}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6">
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
<input
id="additionalStorage"
class="form-control"
type="number"
name="additionalStorageGb"
formControlName="additionalStorage"
step="1"
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
"additionalStorageIntervalDesc"
| i18n
: "1 GB"
: (additionalStoragePriceMonthly(selectedPlan) | currency : "$")
: ("month" | i18n)
}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6" *ngIf="selectedPlan.hasPremiumAccessOption">
<div class="form-check">
<input
id="premiumAccess"
class="form-check-input"
type="checkbox"
name="premiumAccessAddon"
formControlName="premiumAccessAddon"
/>
<label for="premiumAccess" class="form-check-label bold">{{
"premiumAccess" | i18n
}}</label>
</div>
<small class="text-muted form-text">{{
"premiumAccessDesc" | i18n : (3.33 | currency : "$") : ("month" | i18n)
}}</small>
</div>
</div>
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
<input
class="form-check-input"
type="radio"
name="plan"
id="interval{{ selectablePlan.type }}"
[value]="selectablePlan.type"
formControlName="plan"
/>
<label class="form-check-label" for="interval{{ selectablePlan.type }}">
<ng-container *ngIf="selectablePlan.isAnnual">
{{ "annually" | i18n }}
<small *ngIf="selectablePlan.basePrice">
{{ "basePrice" | i18n }}: {{ selectablePlan.basePrice / 12 | currency : "$" }} &times;
12
{{ "monthAbbr" | i18n }}
=
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
<span style="text-decoration: line-through">{{
selectablePlan.basePrice | currency : "$"
}}</span>
{{ "freeWithSponsorship" | i18n }}
</ng-container>
<ng-template #notAcceptingSponsorship>
{{ selectablePlan.basePrice | currency : "$" }}
/{{ "year" | i18n }}
</ng-template>
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{ "additionalUsers" | i18n }}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ selectablePlan.seatPrice / 12 | currency : "$" }} &times; 12
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency : "$" }} /{{
"year" | i18n
}}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ selectablePlan.additionalStoragePricePerGb / 12 | currency : "$" }} &times; 12
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency : "$" }} /{{ "year" | i18n }}
</small>
</ng-container>
<ng-container *ngIf="!selectablePlan.isAnnual">
{{ "monthly" | i18n }}
<small *ngIf="selectablePlan.basePrice">
{{ "basePrice" | i18n }}: {{ selectablePlan.basePrice | currency : "$" }}
{{ "monthAbbr" | i18n }}
=
{{ selectablePlan.basePrice | currency : "$" }}
/{{ "month" | i18n }}
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{ "additionalUsers" | i18n }}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ selectablePlan.seatPrice | currency : "$" }} {{ "monthAbbr" | i18n }} =
{{ seatTotal(selectablePlan) | currency : "$" }} /{{ "month" | i18n }}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ selectablePlan.additionalStoragePricePerGb | currency : "$" }}
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency : "$" }} /{{ "month" | i18n }}
</small>
</ng-container>
</label>
</div>
<hr class="my-3" />
<h2 class="spaced-header mb-4">
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
</h2>
<small class="text-muted font-italic mb-3 d-block">
{{ paymentDesc }}
</small>
<app-payment *ngIf="createOrganization" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
<div id="price" class="my-4">
<div class="text-muted text-sm">
{{ "planPrice" | i18n }}: {{ subtotal | currency : "USD $" }}
<br />
<ng-container>
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency : "USD $" }}
</ng-container>
</div>
<hr class="my-1 col-3 ml-0" />
<p class="text-lg">
<strong>{{ "total" | i18n }}:</strong> {{ total | currency : "USD $" }}/{{
selectedPlanInterval | i18n
}}
</p>
</div>
<ng-container *ngIf="!createOrganization">
<app-payment [showMethods]="false"></app-payment>
</ng-container>
</div>
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
</div>
<div class="mt-4">
<button
type="submit"
buttonType="primary"
bitButton
[loading]="form.loading"
[disabled]="!formGroup.valid"
>
{{ "submit" | i18n }}
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{ "cancel" | i18n }}
</button>
</div>
</form>

View File

@@ -1,521 +0,0 @@
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
import { PlanType } from "@bitwarden/common/enums/planType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { ProductType } from "@bitwarden/common/enums/productType";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { OrganizationCreateRequest } from "@bitwarden/common/models/request/organization-create.request";
import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request";
import { OrganizationUpgradeRequest } from "@bitwarden/common/models/request/organization-upgrade.request";
import { ProviderOrganizationCreateRequest } from "@bitwarden/common/models/request/provider/provider-organization-create.request";
import { PlanResponse } from "@bitwarden/common/models/response/plan.response";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { PaymentComponent } from "./payment.component";
import { TaxInfoComponent } from "./tax-info.component";
interface OnSuccessArgs {
organizationId: string;
}
@Component({
selector: "app-organization-plans",
templateUrl: "organization-plans.component.html",
})
export class OrganizationPlansComponent implements OnInit, OnDestroy {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@Input() organizationId: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() acceptingSponsorship = false;
@Input()
get product(): ProductType {
return this._product;
}
set product(product: ProductType) {
this._product = product;
this.formGroup?.controls?.product?.setValue(product);
}
private _product = ProductType.Free;
@Input()
get plan(): PlanType {
return this._plan;
}
set plan(plan: PlanType) {
this._plan = plan;
this.formGroup?.controls?.plan?.setValue(plan);
}
private _plan = PlanType.Free;
@Input() providerId?: string;
@Output() onSuccess = new EventEmitter<OnSuccessArgs>();
@Output() onCanceled = new EventEmitter<void>();
@Output() onTrialBillingSuccess = new EventEmitter();
loading = true;
selfHosted = false;
productTypes = ProductType;
formPromise: Promise<string>;
singleOrgPolicyAppliesToActiveUser = false;
isInTrialFlow = false;
discount = 0;
formGroup = this.formBuilder.group({
name: [""],
billingEmail: ["", [Validators.email]],
businessOwned: [false],
premiumAccessAddon: [false],
additionalStorage: [0, [Validators.min(0), Validators.max(99)]],
additionalSeats: [0, [Validators.min(0), Validators.max(100000)]],
clientOwnerEmail: ["", [Validators.email]],
businessName: [""],
plan: [this.plan],
product: [this.product],
});
plans: PlanResponse[];
private destroy$ = new Subject<void>();
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService,
private router: Router,
private syncService: SyncService,
private policyService: PolicyService,
private organizationService: OrganizationService,
private logService: LogService,
private messagingService: MessagingService,
private formBuilder: UntypedFormBuilder,
private organizationApiService: OrganizationApiServiceAbstraction
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
if (!this.selfHosted) {
const plans = await this.apiService.getPlans();
this.plans = plans.data;
if (this.product === ProductType.Enterprise || this.product === ProductType.Teams) {
this.formGroup.controls.businessOwned.setValue(true);
}
}
if (this.providerId) {
this.formGroup.controls.businessOwned.setValue(true);
this.changedOwnedBusiness();
}
if (!this.createOrganization || this.acceptingSponsorship) {
this.formGroup.controls.product.setValue(ProductType.Families);
this.changedProduct();
}
if (this.createOrganization) {
this.formGroup.controls.name.addValidators([Validators.required, Validators.maxLength(50)]);
this.formGroup.controls.billingEmail.addValidators(Validators.required);
}
this.policyService
.policyAppliesToActiveUser$(PolicyType.SingleOrg)
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser;
});
this.loading = false;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get singleOrgPolicyBlock() {
return this.singleOrgPolicyAppliesToActiveUser && this.providerId == null;
}
get createOrganization() {
return this.organizationId == null;
}
get selectedPlan() {
return this.plans.find((plan) => plan.type === this.formGroup.controls.plan.value);
}
get selectedPlanInterval() {
return this.selectedPlan.isAnnual ? "year" : "month";
}
get selectableProducts() {
let validPlans = this.plans.filter((plan) => plan.type !== PlanType.Custom);
if (this.formGroup.controls.businessOwned.value) {
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
}
if (!this.showFree) {
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
}
validPlans = validPlans.filter(
(plan) =>
!plan.legacyYear &&
!plan.disabled &&
(plan.isAnnual || plan.product === this.productTypes.Free)
);
if (this.acceptingSponsorship) {
const familyPlan = this.plans.find((plan) => plan.type === PlanType.FamiliesAnnually);
this.discount = familyPlan.basePrice;
validPlans = [familyPlan];
}
return validPlans;
}
get selectablePlans() {
return this.plans?.filter(
(plan) =>
!plan.legacyYear && !plan.disabled && plan.product === this.formGroup.controls.product.value
);
}
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.additionalStoragePricePerGb;
}
return selectedPlan.additionalStoragePricePerGb / 12;
}
seatPriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.seatPrice;
}
return selectedPlan.seatPrice / 12;
}
additionalStorageTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalStorageOption) {
return 0;
}
return (
plan.additionalStoragePricePerGb *
Math.abs(this.formGroup.controls.additionalStorage.value || 0)
);
}
seatTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalSeatsOption) {
return 0;
}
return plan.seatPrice * Math.abs(this.formGroup.controls.additionalSeats.value || 0);
}
get subtotal() {
let subTotal = this.selectedPlan.basePrice;
if (
this.selectedPlan.hasAdditionalSeatsOption &&
this.formGroup.controls.additionalSeats.value
) {
subTotal += this.seatTotal(this.selectedPlan);
}
if (
this.selectedPlan.hasAdditionalStorageOption &&
this.formGroup.controls.additionalStorage.value
) {
subTotal += this.additionalStorageTotal(this.selectedPlan);
}
if (
this.selectedPlan.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value
) {
subTotal += this.selectedPlan.premiumAccessOptionPrice;
}
return subTotal - this.discount;
}
get freeTrial() {
return this.selectedPlan.trialPeriodDays != null;
}
get taxCharges() {
return this.taxComponent != null && this.taxComponent.taxRate != null
? (this.taxComponent.taxRate / 100) * this.subtotal
: 0;
}
get total() {
return this.subtotal + this.taxCharges || 0;
}
get paymentDesc() {
if (this.acceptingSponsorship) {
return this.i18nService.t("paymentSponsored");
} else if (this.freeTrial && this.createOrganization) {
return this.i18nService.t("paymentChargedWithTrial");
} else {
return this.i18nService.t("paymentCharged", this.i18nService.t(this.selectedPlanInterval));
}
}
changedProduct() {
this.formGroup.controls.plan.setValue(this.selectablePlans[0].type);
if (!this.selectedPlan.hasPremiumAccessOption) {
this.formGroup.controls.premiumAccessAddon.setValue(false);
}
if (!this.selectedPlan.hasAdditionalStorageOption) {
this.formGroup.controls.additionalStorage.setValue(0);
}
if (!this.selectedPlan.hasAdditionalSeatsOption) {
this.formGroup.controls.additionalSeats.setValue(0);
} else if (
!this.formGroup.controls.additionalSeats.value &&
!this.selectedPlan.baseSeats &&
this.selectedPlan.hasAdditionalSeatsOption
) {
this.formGroup.controls.additionalSeats.setValue(1);
}
}
changedOwnedBusiness() {
if (!this.formGroup.controls.businessOwned.value || this.selectedPlan.canBeUsedByBusiness) {
return;
}
this.formGroup.controls.product.setValue(ProductType.Teams);
this.formGroup.controls.plan.setValue(PlanType.TeamsAnnually);
this.changedProduct();
}
changedCountry() {
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== "US";
// Bank Account payments are only available for US customers
if (
this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount
) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
cancel() {
this.onCanceled.emit();
}
async submit() {
if (this.singleOrgPolicyBlock) {
return;
}
try {
const doSubmit = async (): Promise<string> => {
let orgId: string = null;
if (this.createOrganization) {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const collection = await this.cryptoService.encrypt(
this.i18nService.t("defaultCollection"),
shareKey[1]
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(shareKey[1]);
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} else {
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, shareKey[1]);
}
this.platformUtilsService.showToast(
"success",
this.i18nService.t("organizationCreated"),
this.i18nService.t("organizationReadyToGo")
);
} else {
orgId = await this.updateOrganization(orgId);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationUpgraded")
);
}
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
this.router.navigate(["/organizations/" + orgId]);
}
if (this.isInTrialFlow) {
this.onTrialBillingSuccess.emit({
orgId: orgId,
subLabelText: this.billingSubLabelText(),
});
}
return orgId;
};
this.formPromise = doSubmit();
const organizationId = await this.formPromise;
this.onSuccess.emit({ organizationId: organizationId });
this.messagingService.send("organizationCreated", organizationId);
} catch (e) {
this.logService.error(e);
}
}
private async updateOrganization(orgId: string) {
const request = new OrganizationUpgradeRequest();
request.businessName = this.formGroup.controls.businessOwned.value
? this.formGroup.controls.businessName.value
: null;
request.additionalSeats = this.formGroup.controls.additionalSeats.value;
request.additionalStorageGb = this.formGroup.controls.additionalStorage.value;
request.premiumAccessAddon =
this.selectedPlan.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
// Retrieve org info to backfill pub/priv key if necessary
const org = await this.organizationService.get(this.organizationId);
if (!org.hasPublicAndPrivateKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
}
const result = await this.organizationApiService.upgrade(this.organizationId, request);
if (!result.success && result.paymentIntentClientSecret != null) {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
}
return this.organizationId;
}
private async createCloudHosted(
key: string,
collectionCt: string,
orgKeys: [string, EncString],
orgKey: SymmetricCryptoKey
) {
const request = new OrganizationCreateRequest();
request.key = key;
request.collectionName = collectionCt;
request.name = this.formGroup.controls.name.value;
request.billingEmail = this.formGroup.controls.billingEmail.value;
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
} else {
const tokenResult = await this.paymentComponent.createPaymentToken();
request.paymentToken = tokenResult[0];
request.paymentMethodType = tokenResult[1];
request.businessName = this.formGroup.controls.businessOwned.value
? this.formGroup.controls.businessName.value
: null;
request.additionalSeats = this.formGroup.controls.additionalSeats.value;
request.additionalStorageGb = this.formGroup.controls.additionalStorage.value;
request.premiumAccessAddon =
this.selectedPlan.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
if (this.taxComponent.taxInfo.includeTaxId) {
request.taxIdNumber = this.taxComponent.taxInfo.taxId;
request.billingAddressLine1 = this.taxComponent.taxInfo.line1;
request.billingAddressLine2 = this.taxComponent.taxInfo.line2;
request.billingAddressCity = this.taxComponent.taxInfo.city;
request.billingAddressState = this.taxComponent.taxInfo.state;
}
}
if (this.providerId) {
const providerRequest = new ProviderOrganizationCreateRequest(
this.formGroup.controls.clientOwnerEmail.value,
request
);
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
providerRequest.organizationCreateRequest.key = (
await this.cryptoService.encrypt(orgKey.key, providerKey)
).encryptedString;
const orgId = (
await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest)
).organizationId;
return orgId;
} else {
return (await this.organizationApiService.create(request)).id;
}
}
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
const fileEl = document.getElementById("file") as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
throw new Error(this.i18nService.t("selectFile"));
}
const fd = new FormData();
fd.append("license", files[0]);
fd.append("key", key);
fd.append("collectionName", collectionCt);
const response = await this.organizationApiService.createLicense(fd);
const orgId = response.id;
await this.apiService.refreshIdentityToken();
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
await this.organizationApiService.updateKeys(orgId, request);
return orgId;
}
private billingSubLabelText(): string {
const selectedPlan = this.selectedPlan;
const price = selectedPlan.basePrice === 0 ? selectedPlan.seatPrice : selectedPlan.basePrice;
let text = "";
if (selectedPlan.isAnnual) {
text += `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`;
} else {
text += `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`;
}
return text;
}
}

View File

@@ -1,134 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="d-flex" [ngClass]="headerClass">
<h1>
{{ "paymentMethod" | i18n }}
</h1>
<button
bitButton
buttonType="secondary"
(click)="load()"
class="tw-ml-auto"
*ngIf="firstLoaded"
[disabled]="loading"
>
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
{{ "refresh" | i18n }}
</button>
</div>
<ng-container *ngIf="!firstLoaded && 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>
<ng-container *ngIf="billing">
<h2>{{ (isCreditBalance ? "accountCredit" : "accountBalance") | i18n }}</h2>
<p class="text-lg">
<strong>{{ creditOrBalance | currency : "$" }}</strong>
</p>
<p>{{ "creditAppliedDesc" | i18n }}</p>
<button bitButton buttonType="secondary" (click)="addCredit()" *ngIf="!showAddCredit">
{{ "addCredit" | i18n }}
</button>
<app-add-credit
[organizationId]="organizationId"
(onAdded)="closeAddCredit(true)"
(onCanceled)="closeAddCredit(false)"
*ngIf="showAddCredit"
>
</app-add-credit>
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
<ng-container *ngIf="paymentSource">
<app-callout
type="warning"
title="{{ 'verifyBankAccount' | i18n }}"
*ngIf="
forOrganization &&
paymentSource.type === paymentMethodType.BankAccount &&
paymentSource.needsVerification
"
>
<p>{{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}</p>
<form
#verifyForm
class="form-inline"
(ngSubmit)="verifyBank()"
[formGroup]="verifyBankForm"
[appApiAction]="verifyBankPromise"
ngNativeValidate
>
<bit-form-field class="tw-mr-2 tw-w-40">
<bit-label>{{ "amountX" | i18n : "1" }}</bit-label>
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount1" />
<span bitPrefix>$0.</span>
</bit-form-field>
<bit-form-field class="tw-mr-2 tw-w-40">
<bit-label>{{ "amountX" | i18n : "2" }}</bit-label>
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount2" />
<span bitPrefix>$0.</span>
</bit-form-field>
<button
bitButton
buttonType="primary"
type="submit"
class="btn-submit"
[disabled]="verifyForm.loading"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "verifyBankAccount" | i18n }}</span>
</button>
</form>
</app-callout>
<p>
<i class="bwi bwi-fw" [ngClass]="paymentSourceClasses"></i>
<span *ngIf="paymentSourceInApp">{{ "inAppPurchase" | i18n }}</span>
{{ paymentSource.description }}
</p>
</ng-container>
<button bitButton buttonType="secondary" (click)="changePayment()" *ngIf="!showAdjustPayment">
{{ (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>
<ng-container *ngIf="forOrganization">
<h2 class="spaced-header">{{ "taxInformation" | i18n }}</h2>
<p>{{ "taxInformationDesc" | i18n }}</p>
<div *ngIf="!org || loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<form
*ngIf="org && !loading"
#formTax
(ngSubmit)="submitTaxInfo()"
[appApiAction]="taxFormPromise"
ngNativeValidate
>
<app-tax-info></app-tax-info>
<button
bitButton
buttonType="primary"
type="submit"
class="btn-submit"
[disabled]="formTax.loading"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
</form>
</ng-container>
</ng-container>

View File

@@ -1,213 +0,0 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.request";
import { BillingPaymentResponse } from "@bitwarden/common/models/response/billing-payment.response";
import { OrganizationResponse } from "@bitwarden/common/models/response/organization.response";
import { TaxInfoComponent } from "./tax-info.component";
@Component({
selector: "app-payment-method",
templateUrl: "payment-method.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PaymentMethodComponent implements OnInit {
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
loading = false;
firstLoaded = false;
showAdjustPayment = false;
showAddCredit = false;
billing: BillingPaymentResponse;
org: OrganizationResponse;
paymentMethodType = PaymentMethodType;
organizationId: string;
verifyBankPromise: Promise<any>;
taxFormPromise: Promise<any>;
verifyBankForm = this.formBuilder.group({
amount1: new FormControl<number>(null, [
Validators.required,
Validators.max(99),
Validators.min(0),
]),
amount2: new FormControl<number>(null, [
Validators.required,
Validators.max(99),
Validators.min(0),
]),
});
constructor(
protected apiService: ApiService,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
private router: Router,
private logService: LogService,
private route: ActivatedRoute,
private formBuilder: FormBuilder
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.params.subscribe(async (params) => {
if (params.organizationId) {
this.organizationId = params.organizationId;
} else if (this.platformUtilsService.isSelfHost()) {
this.router.navigate(["/settings/subscription"]);
return;
}
await this.load();
this.firstLoaded = true;
});
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
if (this.forOrganization) {
const billingPromise = this.organizationApiService.getBilling(this.organizationId);
const orgPromise = this.organizationApiService.get(this.organizationId);
[this.billing, this.org] = await Promise.all([billingPromise, orgPromise]);
} else {
this.billing = await this.apiService.getUserBillingPayment();
}
this.loading = false;
}
addCredit() {
if (this.paymentSourceInApp) {
this.platformUtilsService.showDialog(
this.i18nService.t("cannotPerformInAppPurchase"),
this.i18nService.t("addCredit"),
null,
null,
"warning"
);
return;
}
this.showAddCredit = true;
}
closeAddCredit(load: boolean) {
this.showAddCredit = false;
if (load) {
this.load();
}
}
changePayment() {
if (this.paymentSourceInApp) {
this.platformUtilsService.showDialog(
this.i18nService.t("cannotPerformInAppPurchase"),
this.i18nService.t("changePaymentMethod"),
null,
null,
"warning"
);
return;
}
this.showAdjustPayment = true;
}
closePayment(load: boolean) {
this.showAdjustPayment = false;
if (load) {
this.load();
}
}
async verifyBank() {
if (this.loading || !this.forOrganization) {
return;
}
try {
const request = new VerifyBankRequest();
request.amount1 = this.verifyBankForm.value.amount1;
request.amount2 = this.verifyBankForm.value.amount2;
this.verifyBankPromise = this.organizationApiService.verifyBank(this.organizationId, request);
await this.verifyBankPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("verifiedBankAccount")
);
this.load();
} catch (e) {
this.logService.error(e);
}
}
async submitTaxInfo() {
this.taxFormPromise = this.taxInfo.submitTaxInfo();
await this.taxFormPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("taxInfoUpdated"));
}
get isCreditBalance() {
return this.billing == null || this.billing.balance <= 0;
}
get creditOrBalance() {
return Math.abs(this.billing != null ? this.billing.balance : 0);
}
get paymentSource() {
return this.billing != null ? this.billing.paymentSource : null;
}
get forOrganization() {
return this.organizationId != null;
}
get headerClass() {
return this.forOrganization ? ["page-header"] : ["tabbed-header"];
}
get paymentSourceClasses() {
if (this.paymentSource == null) {
return [];
}
switch (this.paymentSource.type) {
case PaymentMethodType.Card:
return ["bwi-credit-card"];
case PaymentMethodType.BankAccount:
return ["bwi-bank"];
case PaymentMethodType.Check:
return ["bwi-money"];
case PaymentMethodType.AppleInApp:
return ["bwi-apple text-muted"];
case PaymentMethodType.GoogleInApp:
return ["bwi-google text-muted"];
case PaymentMethodType.PayPal:
return ["bwi-paypal text-primary"];
default:
return [];
}
}
get paymentSourceInApp() {
return (
this.paymentSource != null &&
(this.paymentSource.type === PaymentMethodType.AppleInApp ||
this.paymentSource.type === PaymentMethodType.GoogleInApp)
);
}
}

View File

@@ -1,163 +0,0 @@
<div class="mb-4 text-lg" *ngIf="showOptions && showMethods">
<div class="form-check form-check-inline mr-4">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-card"
[value]="paymentMethodType.Card"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-card">
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i> {{ "creditCard" | i18n }}</label
>
</div>
<div class="form-check form-check-inline mr-4" *ngIf="!hideBank">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-bank"
[value]="paymentMethodType.BankAccount"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-bank">
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i> {{ "bankAccount" | i18n }}</label
>
</div>
<div class="form-check form-check-inline" *ngIf="!hidePaypal">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-paypal"
[value]="paymentMethodType.PayPal"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-paypal">
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i> PayPal</label
>
</div>
<div class="form-check form-check-inline" *ngIf="!hideCredit">
<input
class="form-check-input"
type="radio"
name="Method"
id="method-credit"
[value]="paymentMethodType.Credit"
[(ngModel)]="method"
(change)="changeMethod()"
/>
<label class="form-check-label" for="method-credit">
<i class="bwi bwi-fw bwi-dollar" aria-hidden="true"></i> {{ "accountCredit" | i18n }}</label
>
</div>
</div>
<ng-container *ngIf="showMethods && method === paymentMethodType.Card">
<div class="row">
<div [ngClass]="trialFlow ? 'col-5' : 'col-4'" class="form-group">
<label for="stripe-card-number-element">{{ "number" | i18n }}</label>
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
</div>
<div *ngIf="!trialFlow" class="form-group col-8 d-flex align-items-end">
<img
src="../../images/cards.png"
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
width="323"
height="32"
/>
</div>
<div [ngClass]="trialFlow ? 'col-3' : 'col-4'" class="form-group">
<label for="stripe-card-expiry-element">{{ "expiration" | i18n }}</label>
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-4">
<div class="d-flex">
<label for="stripe-card-cvc-element">
{{ "securityCode" | i18n }}
</label>
<a
href="https://www.cvvnumber.com/cvv.html"
tabindex="-1"
target="_blank"
rel="noopener noreferrer"
class="ml-auto"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.BankAccount">
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
</app-callout>
<div class="row">
<div class="form-group col-6">
<label for="routing_number">{{ "routingNumber" | i18n }}</label>
<input
id="routing_number"
class="form-control"
type="text"
name="routing_number"
[(ngModel)]="bank.routing_number"
required
appInputVerbatim
/>
</div>
<div class="form-group col-6">
<label for="account_number">{{ "accountNumber" | i18n }}</label>
<input
id="account_number"
class="form-control"
type="text"
name="account_number"
[(ngModel)]="bank.account_number"
required
appInputVerbatim
/>
</div>
<div class="form-group col-6">
<label for="account_holder_name">{{ "accountHolderName" | i18n }}</label>
<input
id="account_holder_name"
class="form-control"
type="text"
name="account_holder_name"
[(ngModel)]="bank.account_holder_name"
required
/>
</div>
<div class="form-group col-6">
<label for="account_holder_type">{{ "bankAccountType" | i18n }}</label>
<select
id="account_holder_type"
class="form-control"
name="account_holder_type"
[(ngModel)]="bank.account_holder_type"
required
>
<option value="">-- {{ "select" | i18n }} --</option>
<option value="company">{{ "bankAccountTypeCompany" | i18n }}</option>
<option value="individual">{{ "bankAccountTypeIndividual" | i18n }}</option>
</select>
</div>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.PayPal">
<div class="mb-3">
<div id="bt-dropin-container" class="mb-1"></div>
<small class="text-muted">{{ "paypalClickSubmit" | i18n }}</small>
</div>
</ng-container>
<ng-container *ngIf="showMethods && method === paymentMethodType.Credit">
<app-callout type="note">
{{ "makeSureEnoughCredit" | i18n }}
</app-callout>
</ng-container>

View File

@@ -1,288 +0,0 @@
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
@Component({
selector: "app-payment",
templateUrl: "payment.component.html",
})
export class PaymentComponent implements OnInit, OnDestroy {
@Input() showMethods = true;
@Input() showOptions = true;
@Input() method = PaymentMethodType.Card;
@Input() hideBank = false;
@Input() hidePaypal = false;
@Input() hideCredit = false;
@Input() trialFlow = false;
private destroy$ = new Subject<void>();
bank: any = {
routing_number: null,
account_number: null,
account_holder_name: null,
account_holder_type: "",
currency: "USD",
country: "US",
};
paymentMethodType = PaymentMethodType;
private btScript: HTMLScriptElement;
private btInstance: any = null;
private stripeScript: HTMLScriptElement;
private stripe: any = null;
private stripeElements: any = null;
private stripeCardNumberElement: any = null;
private stripeCardExpiryElement: any = null;
private stripeCardCvcElement: any = null;
private StripeElementStyle: any;
private StripeElementClasses: any;
constructor(
private apiService: ApiService,
private logService: LogService,
private themingService: AbstractThemingService
) {
this.stripeScript = window.document.createElement("script");
this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false";
this.stripeScript.async = true;
this.stripeScript.onload = () => {
this.stripe = (window as any).Stripe(process.env.STRIPE_KEY);
this.stripeElements = this.stripe.elements();
this.setStripeElement();
};
this.btScript = window.document.createElement("script");
this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`;
this.btScript.async = true;
this.StripeElementStyle = {
base: {
color: null,
fontFamily:
'"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontSize: "14px",
fontSmoothing: "antialiased",
"::placeholder": {
color: null,
},
},
invalid: {
color: null,
},
};
this.StripeElementClasses = {
focus: "is-focused",
empty: "is-empty",
invalid: "is-invalid",
};
}
async ngOnInit() {
if (!this.showOptions) {
this.hidePaypal = this.method !== PaymentMethodType.PayPal;
this.hideBank = this.method !== PaymentMethodType.BankAccount;
this.hideCredit = this.method !== PaymentMethodType.Credit;
}
this.subscribeToTheme();
window.document.head.appendChild(this.stripeScript);
if (!this.hidePaypal) {
window.document.head.appendChild(this.btScript);
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
window.document.head.removeChild(this.stripeScript);
window.setTimeout(() => {
Array.from(window.document.querySelectorAll("iframe")).forEach((el) => {
if (el.src != null && el.src.indexOf("stripe") > -1) {
try {
window.document.body.removeChild(el);
} catch (e) {
this.logService.error(e);
}
}
});
}, 500);
if (!this.hidePaypal) {
window.document.head.removeChild(this.btScript);
window.setTimeout(() => {
Array.from(window.document.head.querySelectorAll("script")).forEach((el) => {
if (el.src != null && el.src.indexOf("paypal") > -1) {
try {
window.document.head.removeChild(el);
} catch (e) {
this.logService.error(e);
}
}
});
const btStylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet");
if (btStylesheet != null) {
try {
window.document.head.removeChild(btStylesheet);
} catch (e) {
this.logService.error(e);
}
}
}, 500);
}
}
changeMethod() {
this.btInstance = null;
if (this.method === PaymentMethodType.PayPal) {
window.setTimeout(() => {
(window as any).braintree.dropin.create(
{
authorization: process.env.BRAINTREE_KEY,
container: "#bt-dropin-container",
paymentOptionPriority: ["paypal"],
paypal: {
flow: "vault",
buttonStyle: {
label: "pay",
size: "medium",
shape: "pill",
color: "blue",
tagline: "false",
},
},
},
(createErr: any, instance: any) => {
if (createErr != null) {
// eslint-disable-next-line
console.error(createErr);
return;
}
this.btInstance = instance;
}
);
}, 250);
} else {
this.setStripeElement();
}
}
createPaymentToken(): Promise<[string, PaymentMethodType]> {
return new Promise((resolve, reject) => {
if (this.method === PaymentMethodType.Credit) {
resolve([null, this.method]);
} else if (this.method === PaymentMethodType.PayPal) {
this.btInstance
.requestPaymentMethod()
.then((payload: any) => {
resolve([payload.nonce, this.method]);
})
.catch((err: any) => {
reject(err.message);
});
} else if (
this.method === PaymentMethodType.Card ||
this.method === PaymentMethodType.BankAccount
) {
if (this.method === PaymentMethodType.Card) {
this.apiService
.postSetupPayment()
.then((clientSecret) =>
this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement)
)
.then((result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.setupIntent && result.setupIntent.status === "succeeded") {
resolve([result.setupIntent.payment_method, this.method]);
} else {
reject();
}
});
} else {
this.stripe.createToken("bank_account", this.bank).then((result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.token && result.token.id != null) {
resolve([result.token.id, this.method]);
} else {
reject();
}
});
}
}
});
}
handleStripeCardPayment(clientSecret: string, successCallback: () => Promise<any>): Promise<any> {
return new Promise<void>((resolve, reject) => {
if (this.showMethods && this.stripeCardNumberElement == null) {
reject();
return;
}
const handleCardPayment = () =>
this.showMethods
? this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement)
: this.stripe.handleCardSetup(clientSecret);
return handleCardPayment().then(async (result: any) => {
if (result.error) {
reject(result.error.message);
} else if (result.paymentIntent && result.paymentIntent.status === "succeeded") {
if (successCallback != null) {
await successCallback();
}
resolve();
} else {
reject();
}
});
});
}
private setStripeElement() {
window.setTimeout(() => {
if (this.showMethods && this.method === PaymentMethodType.Card) {
if (this.stripeCardNumberElement == null) {
this.stripeCardNumberElement = this.stripeElements.create("cardNumber", {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
placeholder: "",
});
}
if (this.stripeCardExpiryElement == null) {
this.stripeCardExpiryElement = this.stripeElements.create("cardExpiry", {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
});
}
if (this.stripeCardCvcElement == null) {
this.stripeCardCvcElement = this.stripeElements.create("cardCvc", {
style: this.StripeElementStyle,
classes: this.StripeElementClasses,
placeholder: "",
});
}
this.stripeCardNumberElement.mount("#stripe-card-number-element");
this.stripeCardExpiryElement.mount("#stripe-card-expiry-element");
this.stripeCardCvcElement.mount("#stripe-card-cvc-element");
}
}, 50);
}
private subscribeToTheme() {
this.themingService.theme$.pipe(takeUntil(this.destroy$)).subscribe(() => {
const style = getComputedStyle(document.documentElement);
this.StripeElementStyle.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`;
this.StripeElementStyle.base["::placeholder"].color = `rgb(${style.getPropertyValue(
"--color-text-muted"
)})`;
this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`;
this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue(
"--color-danger-500"
)})`;
});
}
}

View File

@@ -10,8 +10,8 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { PaymentComponent } from "./payment.component";
import { TaxInfoComponent } from "./tax-info.component";
import { PaymentComponent } from "../billing/settings/payment.component";
import { TaxInfoComponent } from "../billing/settings/tax-info.component";
@Component({
selector: "app-premium",

View File

@@ -2,8 +2,8 @@ import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { StateService } from "../core";

View File

@@ -1,104 +0,0 @@
<div class="page-header">
<h1>{{ "sponsoredFamilies" | i18n }}</h1>
</div>
<ng-container *ngIf="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="!loading">
<p>
{{ "sponsoredFamiliesEligible" | i18n }}
</p>
<div>
{{ "sponsoredFamiliesInclude" | i18n }}:
<ul class="inset-list">
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
</ul>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="sponsorshipForm"
ngNativeValidate
*ngIf="anyOrgsAvailable$ | async"
>
<div class="form-group col-7">
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
<select
id="availableSponsorshipOrg"
name="Available Sponsorship Organization"
formControlName="selectedSponsorshipOrgId"
class="form-control"
required
>
<option disabled="true" value="">-- {{ "select" | i18n }} --</option>
<option *ngFor="let o of availableSponsorshipOrgs$ | async" [ngValue]="o.id">
{{ o.name }}
</option>
</select>
</div>
<div class="form-group col-7">
<label for="sponsorshipEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
<input
id="sponsorshipEmail"
class="form-control"
inputmode="email"
formControlName="sponsorshipEmail"
name="sponsorshipEmail"
required
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
/>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.notAllowedValue"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "cannotSponsorSelf" | i18n }}
</small>
<small
aria-errormessage="sponsorshipEmail"
*ngIf="sponsorshipEmailControl.errors?.email"
class="error-inline"
role="alert"
>
<i class="bwi bwi-error" aria-hidden="true"></i>
{{ "invalidEmail" | i18n }}
</small>
</div>
<div class="form-group col-7">
<button class="btn btn-primary btn-submit mt-2" type="submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "redeem" | i18n }}</span>
</button>
</div>
</form>
<ng-container *ngIf="anyActiveSponsorships$ | async">
<div class="border-bottom">
<table class="table table-hover table-list">
<thead>
<tr>
<th>{{ "recipient" | i18n }}</th>
<th>{{ "sponsoringOrg" | i18n }}</th>
<th>{{ "status" | i18n }}</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs$ | async">
<tr
sponsoring-org-row
[sponsoringOrg]="o"
[isSelfHosted]="isSelfHosted"
(sponsorshipRemoved)="forceReload()"
></tr>
</ng-container>
</tbody>
</table>
</div>
<small>{{ "sponsoredFamiliesLeaveCopy" | i18n }}</small>
</ng-container>
</ng-container>

View File

@@ -1,125 +0,0 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { map, Observable, Subject, takeUntil } from "rxjs";
import { notAllowedValueAsync } from "@bitwarden/angular/validators/notAllowedValueAsync.validator";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
interface RequestSponsorshipForm {
selectedSponsorshipOrgId: FormControl<string>;
sponsorshipEmail: FormControl<string>;
}
@Component({
selector: "app-sponsored-families",
templateUrl: "sponsored-families.component.html",
})
export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
loading = false;
availableSponsorshipOrgs$: Observable<Organization[]>;
activeSponsorshipOrgs$: Observable<Organization[]>;
anyOrgsAvailable$: Observable<boolean>;
anyActiveSponsorships$: Observable<boolean>;
// Conditional display properties
formPromise: Promise<void>;
sponsorshipForm: FormGroup<RequestSponsorshipForm>;
private _destroy = new Subject<void>();
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private syncService: SyncService,
private organizationService: OrganizationService,
private formBuilder: FormBuilder,
private stateService: StateService
) {
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
selectedSponsorshipOrgId: new FormControl("", {
validators: [Validators.required],
}),
sponsorshipEmail: new FormControl("", {
validators: [Validators.email],
asyncValidators: [
notAllowedValueAsync(async () => await this.stateService.getEmail(), true),
],
updateOn: "blur",
}),
});
}
async ngOnInit() {
this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable))
);
this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
if (orgs.length === 1) {
this.sponsorshipForm.patchValue({
selectedSponsorshipOrgId: orgs[0].id,
});
}
});
this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
this.activeSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null))
);
this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
this.loading = false;
}
ngOnDestroy(): void {
this._destroy.next();
this._destroy.complete();
}
async submit() {
this.formPromise = this.apiService.postCreateSponsorship(
this.sponsorshipForm.value.selectedSponsorshipOrgId,
{
sponsoredEmail: this.sponsorshipForm.value.sponsorshipEmail,
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
friendlyName: this.sponsorshipForm.value.sponsorshipEmail,
}
);
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated"));
this.formPromise = null;
this.resetForm();
await this.forceReload();
}
async forceReload() {
this.loading = true;
await this.syncService.fullSync(true);
this.loading = false;
}
get sponsorshipEmailControl() {
return this.sponsorshipForm.controls.sponsorshipEmail;
}
private async resetForm() {
this.sponsorshipForm.reset();
}
get isSelfHosted(): boolean {
return this.platformUtilsService.isSelfHost();
}
}

View File

@@ -1,50 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<td>
{{ sponsoringOrg.familySponsorshipFriendlyName }}
</td>
<td>{{ sponsoringOrg.name }}</td>
<td>
<span [ngClass]="statusClass">{{ statusMessage }}</span>
</td>
<td class="table-action-right">
<div class="dropdown" appListDropdown>
<button
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<button
#resendEmailBtn
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
[appApiAction]="resendEmailPromise"
class="dropdown-item btn-submit"
[disabled]="$any(resendEmailBtn).loading"
(click)="resendEmail()"
[attr.aria-label]="'resendEmailLabel' | i18n : sponsoringOrg.familySponsorshipFriendlyName"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "resendEmail" | i18n }}</span>
</button>
<button
#revokeSponsorshipBtn
[appApiAction]="revokeSponsorshipPromise"
class="dropdown-item text-danger btn-submit"
[disabled]="$any(revokeSponsorshipBtn).loading"
(click)="revokeSponsorship()"
[attr.aria-label]="'revokeAccount' | i18n : sponsoringOrg.familySponsorshipFriendlyName"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "remove" | i18n }}</span>
</button>
</div>
</div>
</td>

View File

@@ -1,135 +0,0 @@
import { formatDate } from "@angular/common";
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
@Component({
selector: "[sponsoring-org-row]",
templateUrl: "sponsoring-org-row.component.html",
})
export class SponsoringOrgRowComponent implements OnInit {
@Input() sponsoringOrg: Organization = null;
@Input() isSelfHosted = false;
@Output() sponsorshipRemoved = new EventEmitter();
statusMessage = "loading";
statusClass: "text-success" | "text-danger" = "text-success";
revokeSponsorshipPromise: Promise<any>;
resendEmailPromise: Promise<any>;
private locale = "";
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private logService: LogService,
private platformUtilsService: PlatformUtilsService
) {}
async ngOnInit() {
this.locale = await firstValueFrom(this.i18nService.locale$);
this.setStatus(
this.isSelfHosted,
this.sponsoringOrg.familySponsorshipToDelete,
this.sponsoringOrg.familySponsorshipValidUntil,
this.sponsoringOrg.familySponsorshipLastSyncDate
);
}
async revokeSponsorship() {
try {
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
await this.revokeSponsorshipPromise;
} catch (e) {
this.logService.error(e);
}
this.revokeSponsorshipPromise = null;
}
async resendEmail() {
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
await this.resendEmailPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent"));
this.resendEmailPromise = null;
}
get isSentAwaitingSync() {
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
}
private async doRevokeSponsorship() {
const isConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("revokeSponsorshipConfirmation"),
`${this.i18nService.t("remove")} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`,
this.i18nService.t("remove"),
this.i18nService.t("cancel"),
"warning"
);
if (!isConfirmed) {
return;
}
await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id);
this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan"));
this.sponsorshipRemoved.emit();
}
private setStatus(
selfHosted: boolean,
toDelete?: boolean,
validUntil?: Date,
lastSyncDate?: Date
) {
/*
* Possible Statuses:
* Requested (self-hosted only)
* Sent
* Active
* RequestRevoke
* RevokeWhenExpired
*/
if (toDelete && validUntil) {
// They want to delete but there is a valid until date which means there is an active sponsorship
this.statusMessage = this.i18nService.t(
"revokeWhenExpired",
formatDate(validUntil, "MM/dd/yyyy", this.locale)
);
this.statusClass = "text-danger";
} else if (toDelete) {
// They want to delete and we don't have a valid until date so we can
// this should only happen on a self-hosted install
this.statusMessage = this.i18nService.t("requestRemoved");
this.statusClass = "text-danger";
} else if (validUntil) {
// They don't want to delete and they have a valid until date
// that means they are actively sponsoring someone
this.statusMessage = this.i18nService.t("active");
this.statusClass = "text-success";
} else if (selfHosted && lastSyncDate) {
// We are on a self-hosted install and it has been synced but we have not gotten
// a valid until date so we can't know if they are actively sponsoring someone
this.statusMessage = this.i18nService.t("sent");
this.statusClass = "text-success";
} else if (!selfHosted) {
// We are in cloud and all other status checks have been false therefore we have
// sent the request but it hasn't been accepted yet
this.statusMessage = this.i18nService.t("sent");
this.statusClass = "text-success";
} else {
// We are on a self-hosted install and we have not synced yet
this.statusMessage = this.i18nService.t("requested");
this.statusClass = "text-success";
}
}
}

View File

@@ -1,45 +0,0 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
import { PaymentMethodComponent } from "./payment-method.component";
import { PremiumComponent } from "./premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
const routes: Routes = [
{
path: "",
component: SubscriptionComponent,
data: { titleId: "subscription" },
children: [
{ path: "", pathMatch: "full", redirectTo: "premium" },
{
path: "user-subscription",
component: UserSubscriptionComponent,
data: { titleId: "premiumMembership" },
},
{
path: "premium",
component: PremiumComponent,
data: { titleId: "goPremium" },
},
{
path: "payment-method",
component: PaymentMethodComponent,
data: { titleId: "paymentMethod" },
},
{
path: "billing-history",
component: BillingHistoryViewComponent,
data: { titleId: "billingHistory" },
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SubscriptionRoutingModule {}

View File

@@ -1,20 +0,0 @@
<div class="tabbed-nav d-flex flex-column" *ngIf="!selfHosted">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" [routerLink]="subscriptionRoute" routerLinkActive="active">
{{ "subscription" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="payment-method" routerLinkActive="active">
{{ "paymentMethod" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="billing-history" routerLinkActive="active">
{{ "billingHistory" | i18n }}
</a>
</li>
</ul>
</div>
<router-outlet></router-outlet>

View File

@@ -1,27 +0,0 @@
import { Component } from "@angular/core";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
@Component({
selector: "app-subscription",
templateUrl: "subscription.component.html",
})
export class SubscriptionComponent {
hasPremium: boolean;
selfHosted: boolean;
constructor(
private stateService: StateService,
private platformUtilsService: PlatformUtilsService
) {}
async ngOnInit() {
this.hasPremium = await this.stateService.getHasPremiumPersonally();
this.selfHosted = this.platformUtilsService.isSelfHost();
}
get subscriptionRoute(): string {
return this.hasPremium ? "user-subscription" : "premium";
}
}

View File

@@ -1,356 +0,0 @@
<div class="row">
<div [ngClass]="trialFlow ? 'col-7' : 'col-6'">
<div class="form-group">
<label for="addressCountry">{{ "country" | i18n }}</label>
<select
id="addressCountry"
class="form-control"
[(ngModel)]="taxInfo.country"
required
name="addressCountry"
autocomplete="country"
(change)="changeCountry()"
>
<option value="">-- Select --</option>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="FR">France</option>
<option value="DE">Germany</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="IN">India</option>
<option value="-" disabled></option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia, Plurinational State of</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo</option>
<option value="CD">Congo, the Democratic Republic of the</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands (Malvinas)</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Territories</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="VA">Holy See (Vatican City State)</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran, Islamic Republic of</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, Democratic People's Republic of</option>
<option value="KR">Korea, Republic of</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Lao People's Democratic Republic</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macao</option>
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia, Federated States of</option>
<option value="MD">Moldova, Republic of</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestinian Territory, Occupied</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Réunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VE">Venezuela, Bolivarian Republic of</option>
<option value="VN">Viet Nam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
</select>
</div>
</div>
<div [ngClass]="trialFlow ? 'col-5' : 'col-3'">
<div class="form-group">
<label for="addressPostalCode">{{ "zipPostalCode" | i18n }}</label>
<input
id="addressPostalCode"
class="form-control"
type="text"
name="addressPostalCode"
[(ngModel)]="taxInfo.postalCode"
[required]="taxInfo.country === 'US'"
autocomplete="postal-code"
/>
</div>
</div>
<div class="col-6" *ngIf="organizationId && taxInfo.country !== 'US'">
<div class="form-group form-check">
<input
class="form-check-input"
id="addressIncludeTaxId"
name="addressIncludeTaxId"
type="checkbox"
[(ngModel)]="taxInfo.includeTaxId"
/>
<label class="form-check-label" for="addressIncludeTaxId">{{ "includeVAT" | i18n }}</label>
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="taxId">{{ "taxIdNumber" | i18n }}</label>
<input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId" />
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="addressLine1">{{ "address1" | i18n }}</label>
<input
id="addressLine1"
class="form-control"
type="text"
name="addressLine1"
[(ngModel)]="taxInfo.line1"
autocomplete="address-line1"
/>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressLine2">{{ "address2" | i18n }}</label>
<input
id="addressLine2"
class="form-control"
type="text"
name="addressLine2"
[(ngModel)]="taxInfo.line2"
autocomplete="address-line2"
/>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressCity">{{ "cityTown" | i18n }}</label>
<input
id="addressCity"
class="form-control"
type="text"
name="addressCity"
[(ngModel)]="taxInfo.city"
autocomplete="address-level2"
/>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressState">{{ "stateProvince" | i18n }}</label>
<input
id="addressState"
class="form-control"
type="text"
name="addressState"
[(ngModel)]="taxInfo.state"
autocomplete="address-level1"
/>
</div>
</div>
</div>

View File

@@ -1,181 +0,0 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationTaxInfoUpdateRequest } from "@bitwarden/common/models/request/organization-tax-info-update.request";
import { TaxInfoUpdateRequest } from "@bitwarden/common/models/request/tax-info-update.request";
import { TaxInfoResponse } from "@bitwarden/common/models/response/tax-info.response";
import { TaxRateResponse } from "@bitwarden/common/models/response/tax-rate.response";
type TaxInfoView = Omit<TaxInfoResponse, "taxIdType"> & {
includeTaxId: boolean;
[key: string]: unknown;
};
@Component({
selector: "app-tax-info",
templateUrl: "tax-info.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class TaxInfoComponent {
@Input() trialFlow = false;
@Output() onCountryChanged = new EventEmitter();
loading = true;
organizationId: string;
taxInfo: TaxInfoView = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: "US",
includeTaxId: false,
};
taxRates: TaxRateResponse[];
private pristine: TaxInfoView = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: "US",
includeTaxId: false,
};
constructor(
private apiService: ApiService,
private route: ActivatedRoute,
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
if (this.organizationId) {
try {
const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId);
if (taxInfo) {
this.taxInfo.taxId = taxInfo.taxId;
this.taxInfo.state = taxInfo.state;
this.taxInfo.line1 = taxInfo.line1;
this.taxInfo.line2 = taxInfo.line2;
this.taxInfo.city = taxInfo.city;
this.taxInfo.state = taxInfo.state;
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || "US";
this.taxInfo.includeTaxId =
this.taxInfo.country !== "US" &&
(!!taxInfo.taxId ||
!!taxInfo.line1 ||
!!taxInfo.line2 ||
!!taxInfo.city ||
!!taxInfo.state);
}
} catch (e) {
this.logService.error(e);
}
} else {
try {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || "US";
}
} catch (e) {
this.logService.error(e);
}
}
this.pristine = Object.assign({}, this.taxInfo);
// If not the default (US) then trigger onCountryChanged
if (this.taxInfo.country !== "US") {
this.onCountryChanged.emit();
}
});
try {
const taxRates = await this.apiService.getTaxRates();
if (taxRates) {
this.taxRates = taxRates.data;
}
} catch (e) {
this.logService.error(e);
} finally {
this.loading = false;
}
}
get taxRate() {
if (this.taxRates != null) {
const localTaxRate = this.taxRates.find(
(x) => x.country === this.taxInfo.country && x.postalCode === this.taxInfo.postalCode
);
return localTaxRate?.rate ?? null;
}
}
getTaxInfoRequest(): TaxInfoUpdateRequest {
if (this.organizationId) {
const request = new OrganizationTaxInfoUpdateRequest();
request.taxId = this.taxInfo.taxId;
request.state = this.taxInfo.state;
request.line1 = this.taxInfo.line1;
request.line2 = this.taxInfo.line2;
request.city = this.taxInfo.city;
request.state = this.taxInfo.state;
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
} else {
const request = new TaxInfoUpdateRequest();
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
}
}
submitTaxInfo(): Promise<any> {
if (!this.hasChanged()) {
return new Promise<void>((resolve) => {
resolve();
});
}
const request = this.getTaxInfoRequest();
return this.organizationId
? this.organizationApiService.updateTaxInfo(
this.organizationId,
request as OrganizationTaxInfoUpdateRequest
)
: this.apiService.putTaxInfo(request);
}
changeCountry() {
if (this.taxInfo.country === "US") {
this.taxInfo.includeTaxId = false;
this.taxInfo.taxId = null;
this.taxInfo.line1 = null;
this.taxInfo.line2 = null;
this.taxInfo.city = null;
this.taxInfo.state = null;
}
this.onCountryChanged.emit();
}
private hasChanged(): boolean {
for (const key in this.taxInfo) {
// eslint-disable-next-line
if (this.pristine.hasOwnProperty(key) && this.pristine[key] !== this.taxInfo[key]) {
return true;
}
}
return false;
}
}

View File

@@ -3,8 +3,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
@Component({
selector: "app-update-license",

View File

@@ -1,197 +0,0 @@
<div
[ngClass]="{
'page-header': selfHosted,
'tabbed-header': !selfHosted
}"
>
<h1>
{{ title }}
<small *ngIf="firstLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</small>
</h1>
</div>
<ng-container *ngIf="!firstLoaded && 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>
<ng-container *ngIf="sub">
<bit-callout
type="warning"
title="{{ 'canceled' | i18n }}"
*ngIf="subscription && subscription.cancelled"
>
{{ "subscriptionCanceled" | i18n }}</bit-callout
>
<bit-callout
type="warning"
title="{{ 'pendingCancellation' | i18n }}"
*ngIf="subscriptionMarkedForCancel"
>
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
<button
bitButton
type="button"
buttonType="secondary"
#reinstateBtn
class="btn-submit"
(click)="reinstate()"
[appApiAction]="reinstatePromise"
[disabled]="$any(reinstateBtn).loading"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "reinstateSubscription" | i18n }}</span>
</button>
</bit-callout>
<dl *ngIf="selfHosted">
<dt>{{ "expiration" | i18n }}</dt>
<dd *ngIf="sub.expiration">{{ sub.expiration | date : "mediumDate" }}</dd>
<dd *ngIf="!sub.expiration">{{ "neverExpires" | i18n }}</dd>
</dl>
<div class="row" *ngIf="!selfHosted">
<div class="col-4">
<dl>
<dt>{{ "status" | i18n }}</dt>
<dd>
<span class="text-capitalize">{{ (subscription && subscription.status) || "-" }}</span>
<span bitBadge badgeType="warning" *ngIf="subscriptionMarkedForCancel">{{
"pendingCancellation" | i18n
}}</span>
</dd>
<dt>{{ "nextCharge" | i18n }}</dt>
<dd>
{{
nextInvoice
? (nextInvoice.date | date : "mediumDate") +
", " +
(nextInvoice.amount | currency : "$")
: "-"
}}
</dd>
</dl>
</div>
<div class="col-8" *ngIf="subscription">
<strong class="d-block mb-1">{{ "details" | i18n }}</strong>
<table class="table">
<tbody>
<tr *ngFor="let i of subscription.items">
<td>
{{ i.name }} {{ i.quantity > 1 ? "&times;" + i.quantity : "" }} @
{{ i.amount | currency : "$" }}
</td>
<td>{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<ng-container *ngIf="selfHosted">
<div>
<button type="button" bitButton buttonType="secondary" (click)="updateLicense()">
{{ "updateLicense" | i18n }}
</button>
<a
bitButton
buttonType="secondary"
href="https://vault.bitwarden.com/#/settings/subscription"
target="_blank"
rel="noopener"
>
{{ "launchCloudSubscription" | i18n }}
</a>
</div>
<div class="card mt-3" *ngIf="showUpdateLicense">
<div class="card-body">
<button
type="button"
class="close"
appA11yTitle="{{ 'cancel' | i18n }}"
(click)="closeUpdateLicense(false)"
>
<span aria-hidden="true">&times;</span>
</button>
<h3 class="card-body-header">{{ "updateLicense" | i18n }}</h3>
<app-update-license
(onUpdated)="closeUpdateLicense(true)"
(onCanceled)="closeUpdateLicense(false)"
>
</app-update-license>
</div>
</div>
</ng-container>
<ng-container *ngIf="!selfHosted">
<div class="d-flex">
<button
bitButton
type="button"
buttonType="secondary"
(click)="downloadLicense()"
*ngIf="!subscription || !subscription.cancelled"
>
{{ "downloadLicense" | i18n }}
</button>
<button
bitButton
#cancelBtn
type="button"
buttonType="danger"
class="btn-submit tw-ml-auto"
(click)="cancel()"
[appApiAction]="cancelPromise"
[disabled]="$any(cancelBtn).loading"
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "cancelSubscription" | i18n }}</span>
</button>
</div>
<h2 class="spaced-header">{{ "storage" | i18n }}</h2>
<p>{{ "subscriptionStorage" | i18n : sub.maxStorageGb || 0 : sub.storageName || "0 MB" }}</p>
<div class="progress">
<div
class="progress-bar bg-success"
role="progressbar"
[ngStyle]="{ width: storageProgressWidth + '%' }"
[attr.aria-valuenow]="storagePercentage"
aria-valuemin="0"
aria-valuemax="100"
>
{{ storagePercentage / 100 | percent }}
</div>
</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)">
{{ "addStorage" | i18n }}
</button>
<button
bitButton
type="button"
buttonType="secondary"
class="tw-ml-1"
(click)="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>
</ng-container>

View File

@@ -1,217 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SubscriptionResponse } from "@bitwarden/common/models/response/subscription.response";
@Component({
selector: "app-user-subscription",
templateUrl: "user-subscription.component.html",
})
export class UserSubscriptionComponent implements OnInit {
loading = false;
firstLoaded = false;
adjustStorageAdd = true;
showAdjustStorage = false;
showUpdateLicense = false;
sub: SubscriptionResponse;
selfHosted = false;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
constructor(
private stateService: StateService,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private router: Router,
private logService: LogService,
private fileDownloadService: FileDownloadService
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
await this.load();
this.firstLoaded = true;
}
async load() {
if (this.loading) {
return;
}
if (this.stateService.getHasPremiumPersonally()) {
this.loading = true;
this.sub = await this.apiService.getUserSubscription();
} else {
this.router.navigate(["/settings/subscription/premium"]);
return;
}
this.loading = false;
}
async reinstate() {
if (this.loading) {
return;
}
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(
this.i18nService.t("manageSubscriptionFromStore"),
this.i18nService.t("cancelSubscription"),
null,
null,
"warning"
);
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("reinstateConfirmation"),
this.i18nService.t("reinstateSubscription"),
this.i18nService.t("yes"),
this.i18nService.t("cancel")
);
if (!confirmed) {
return;
}
try {
this.reinstatePromise = this.apiService.postReinstatePremium();
await this.reinstatePromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("reinstated"));
this.load();
} catch (e) {
this.logService.error(e);
}
}
async cancel() {
if (this.loading) {
return;
}
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(
this.i18nService.t("manageSubscriptionFromStore"),
this.i18nService.t("cancelSubscription"),
null,
null,
"warning"
);
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("cancelConfirmation"),
this.i18nService.t("cancelSubscription"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
try {
this.cancelPromise = this.apiService.postCancelPremium();
await this.cancelPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("canceledSubscription")
);
this.load();
} catch (e) {
this.logService.error(e);
}
}
downloadLicense() {
if (this.loading) {
return;
}
const licenseString = JSON.stringify(this.sub.license, null, 2);
this.fileDownloadService.download({
fileName: "bitwarden_premium_license.json",
blobData: licenseString,
});
}
updateLicense() {
if (this.loading) {
return;
}
this.showUpdateLicense = true;
}
closeUpdateLicense(load: boolean) {
this.showUpdateLicense = false;
if (load) {
this.load();
}
}
adjustStorage(add: boolean) {
if (this.usingInAppPurchase) {
this.platformUtilsService.showDialog(
this.i18nService.t("cannotPerformInAppPurchase"),
this.i18nService.t(add ? "addStorage" : "removeStorage"),
null,
null,
"warning"
);
return;
}
this.adjustStorageAdd = add;
this.showAdjustStorage = true;
}
closeStorage(load: boolean) {
this.showAdjustStorage = false;
if (load) {
this.load();
}
}
get subscriptionMarkedForCancel() {
return (
this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate
);
}
get subscription() {
return this.sub != null ? this.sub.subscription : null;
}
get nextInvoice() {
return this.sub != null ? this.sub.upcomingInvoice : null;
}
get storagePercentage() {
return this.sub != null && this.sub.maxStorageGb
? +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2)
: 0;
}
get storageProgressWidth() {
return this.storagePercentage < 5 ? 5 : 0;
}
get usingInAppPurchase() {
return this.sub != null ? this.sub.usingInAppPurchase : false;
}
get title(): string {
return this.i18nService.t(this.selfHosted ? "subscription" : "premiumMembership");
}
}