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:
@@ -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">×</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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="card-body-header">
|
||||
{{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
|
||||
</h3>
|
||||
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">×</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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 : "$" }} ×
|
||||
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 }} ×
|
||||
{{ selectablePlan.seatPrice / 12 | currency : "$" }} × 12
|
||||
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency : "$" }} /{{
|
||||
"year" | i18n
|
||||
}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{ selectablePlan.additionalStoragePricePerGb / 12 | currency : "$" }} × 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 }} ×
|
||||
{{ selectablePlan.seatPrice | currency : "$" }} {{ "monthAbbr" | i18n }} =
|
||||
{{ seatTotal(selectablePlan) | currency : "$" }} /{{ "month" | i18n }}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{ 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
)})`;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ? "×" + 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">×</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>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user