shared org plans component
This commit is contained in:
parent
5dc00a8bc6
commit
0f3d71a504
|
@ -106,6 +106,7 @@ import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.co
|
||||||
import { DeleteAccountComponent } from './settings/delete-account.component';
|
import { DeleteAccountComponent } from './settings/delete-account.component';
|
||||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||||
import { OptionsComponent } from './settings/options.component';
|
import { OptionsComponent } from './settings/options.component';
|
||||||
|
import { OrganizationPlansComponent } from './settings/organization-plans.component';
|
||||||
import { OrganizationsComponent } from './settings/organizations.component';
|
import { OrganizationsComponent } from './settings/organizations.component';
|
||||||
import { PaymentComponent } from './settings/payment.component';
|
import { PaymentComponent } from './settings/payment.component';
|
||||||
import { PremiumComponent } from './settings/premium.component';
|
import { PremiumComponent } from './settings/premium.component';
|
||||||
|
@ -284,6 +285,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||||
OrgAddEditComponent,
|
OrgAddEditComponent,
|
||||||
OrgApiKeyComponent,
|
OrgApiKeyComponent,
|
||||||
OrganizationBillingComponent,
|
OrganizationBillingComponent,
|
||||||
|
OrganizationPlansComponent,
|
||||||
OrganizationSubscriptionComponent,
|
OrganizationSubscriptionComponent,
|
||||||
OrgAttachmentsComponent,
|
OrgAttachmentsComponent,
|
||||||
OrgCiphersComponent,
|
OrgCiphersComponent,
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<div class="card card-org-plans">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
<button type="button" class="close" attr.aria-label="{{'cancel' | i18n}}" title="{{'cancel' | i18n}}"
|
||||||
(click)="cancel()"><span aria-hidden="true">×</span></button>
|
(click)="cancel()"><span aria-hidden="true">×</span></button>
|
||||||
<h3 class="card-body-header">{{'changeBillingPlan' | i18n}}</h3>
|
<h2 class="card-body-header">{{'changeBillingPlan' | i18n}}</h2>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<app-organization-plans [showFree]="false" [showCancel]="true" plan="families" [organizationId]="organizationId"
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
(onCanceled)="cancel()">
|
||||||
<span>{{'submit' | i18n}}</span>
|
</app-organization-plans>
|
||||||
</button>
|
</div>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
|
||||||
{{'cancel' | i18n}}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
|
@ -2,214 +2,4 @@
|
||||||
<h1>{{'newOrganization' | i18n}}</h1>
|
<h1>{{'newOrganization' | i18n}}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>{{'newOrganizationDesc' | i18n}}</p>
|
<p>{{'newOrganizationDesc' | i18n}}</p>
|
||||||
<ng-container *ngIf="selfHosted">
|
<app-organization-plans></app-organization-plans>
|
||||||
<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="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
|
||||||
<span>{{'submit' | i18n}}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</ng-container>
|
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
|
||||||
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="name">{{'organizationName' | i18n}}</label>
|
|
||||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
|
||||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group form-check">
|
|
||||||
<input id="ownedBusiness" class="form-check-input" type="checkbox" name="OwnedBusiness"
|
|
||||||
[(ngModel)]="ownedBusiness" (change)="changedOwnedBusiness()">
|
|
||||||
<label for="ownedBusiness" class="form-check-label">{{'accountOwnedBusiness' | i18n}}</label>
|
|
||||||
</div>
|
|
||||||
<div class="row" *ngIf="ownedBusiness">
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
|
||||||
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="businessName">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
|
|
||||||
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
|
|
||||||
<input class="form-check-input" type="radio" name="PlanType" id="planFree" value="free" [(ngModel)]="plan"
|
|
||||||
(change)="changedPlan()">
|
|
||||||
<label class="form-check-label" for="planFree">
|
|
||||||
{{'planNameFree' | i18n}}
|
|
||||||
<small class="mb-1">{{'planDescFree' | i18n : '1'}}</small>
|
|
||||||
<small>• {{'limitedUsers' | i18n : '2'}}</small>
|
|
||||||
<small>• {{'limitedCollections' | i18n : '2'}}</small>
|
|
||||||
<span>{{'freeForever' | i18n}}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
|
|
||||||
<input class="form-check-input" type="radio" name="PlanType" id="planFamilies" value="families"
|
|
||||||
[(ngModel)]="plan" (change)="changedPlan()">
|
|
||||||
<label class="form-check-label" for="planFamilies">
|
|
||||||
{{'planNameFamilies' | i18n}}
|
|
||||||
<small class="mb-1">{{'planDescFamilies' | i18n}}</small>
|
|
||||||
<small>• {{'addShareLimitedUsers' | i18n : '5'}}</small>
|
|
||||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
|
||||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
|
||||||
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
|
||||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
|
||||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
|
||||||
<span>{{1 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-block">
|
|
||||||
<input class="form-check-input" type="radio" name="PlanType" id="planTeams" value="teams" [(ngModel)]="plan"
|
|
||||||
(change)="changedPlan()">
|
|
||||||
<label class="form-check-label" for="planTeams">
|
|
||||||
{{'planNameTeams' | i18n}}
|
|
||||||
<small class="mb-1">{{'planDescTeams' | i18n}}</small>
|
|
||||||
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
|
||||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
|
||||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
|
||||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
|
||||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
|
||||||
<span>{{5 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}},
|
|
||||||
{{('additionalUsers' | i18n).toLowerCase()}}
|
|
||||||
{{2 | currency:'$'}} /{{'month' | i18n}}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-block">
|
|
||||||
<input class="form-check-input" type="radio" name="PlanType" id="planEnterprise" value="enterprise"
|
|
||||||
[(ngModel)]="plan" (change)="changedPlan()">
|
|
||||||
<label class="form-check-label" for="planEnterprise">
|
|
||||||
{{'planNameEnterprise' | i18n}}
|
|
||||||
<small class="mb-1">{{'planDescEnterprise' | i18n}}</small>
|
|
||||||
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
|
||||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
|
||||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
|
||||||
<small>• {{'controlAccessWithGroups' | i18n}}</small>
|
|
||||||
<small>• {{'trackAuditLogs' | i18n}}</small>
|
|
||||||
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
|
|
||||||
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
|
||||||
<small>• {{'usersGetPremium' | i18n}}</small>
|
|
||||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
|
||||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
|
||||||
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="!plans[plan].noPayment">
|
|
||||||
<ng-container *ngIf="!plans[plan].noAdditionalSeats && !plans[plan].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"
|
|
||||||
[(ngModel)]="additionalSeats" min="1" max="100000" 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="!plans[plan].noAdditionalSeats && plans[plan].baseSeats">
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="additionalSeats">{{'additionalUserSeats' | i18n}}</label>
|
|
||||||
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
|
|
||||||
[(ngModel)]="additionalSeats" min="0" max="100000" placeholder="{{'userSeatsDesc' | i18n}}">
|
|
||||||
<small
|
|
||||||
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | 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"
|
|
||||||
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
|
|
||||||
placeholder="{{'additionalStorageGbDesc' | i18n}}">
|
|
||||||
<small
|
|
||||||
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
|
|
||||||
<div class="form-check">
|
|
||||||
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
|
|
||||||
[(ngModel)]="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">
|
|
||||||
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalAnnually" value="year"
|
|
||||||
[(ngModel)]="interval">
|
|
||||||
<label class="form-check-label" for="intervalAnnually">
|
|
||||||
{{'annually' | i18n}}
|
|
||||||
<small *ngIf="plans[plan].annualBasePrice">
|
|
||||||
{{'basePrice' | i18n}}: {{plans[plan].basePrice | currency:'$'}} ×12 {{'monthAbbr' | i18n}} =
|
|
||||||
{{baseTotal(true) | currency:'$'}}
|
|
||||||
/{{'year' | i18n}}
|
|
||||||
</small>
|
|
||||||
<small *ngIf="!plans[plan].noAdditionalSeats">
|
|
||||||
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
|
||||||
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
|
||||||
{{additionalSeats || 0}} × {{plans[plan].seatPrice | currency:'$'}} ×12
|
|
||||||
{{'monthAbbr' | i18n}} = {{seatTotal(true)
|
|
||||||
| currency:'$'}} /{{'year' | i18n}}
|
|
||||||
</small>
|
|
||||||
<small>
|
|
||||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
|
||||||
{{storageGb.price | currency:'$'}} ×12 {{'monthAbbr'
|
|
||||||
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
|
|
||||||
</small>
|
|
||||||
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
|
|
||||||
{{'premiumAccess' | i18n}}:
|
|
||||||
{{3.33 | currency:'$'}} ×12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
|
|
||||||
</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">
|
|
||||||
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalMonthly" value="month"
|
|
||||||
[(ngModel)]="interval">
|
|
||||||
<label class="form-check-label" for="intervalMonthly">
|
|
||||||
{{'monthly' | i18n}}
|
|
||||||
<small *ngIf="plans[plan].monthlyBasePrice">
|
|
||||||
{{'basePrice' | i18n}}: {{baseTotal(false) | currency:'$'}} /{{'month' | i18n}}
|
|
||||||
</small>
|
|
||||||
<small *ngIf="!plans[plan].noAdditionalSeats">
|
|
||||||
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
|
||||||
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
|
||||||
{{additionalSeats || 0}} × {{plans[plan].monthlySeatPrice | currency:'$'}} =
|
|
||||||
{{seatTotal(false) | currency:'$'}} /{{'month'
|
|
||||||
| i18n}}
|
|
||||||
</small>
|
|
||||||
<small>
|
|
||||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
|
||||||
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
|
|
||||||
| currency:'$'}} /{{'month' | i18n}}
|
|
||||||
</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<hr class="my-3">
|
|
||||||
<div class="text-lg">
|
|
||||||
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
|
|
||||||
</div>
|
|
||||||
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
|
|
||||||
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
|
|
||||||
<app-payment [hideCredit]="true"></app-payment>
|
|
||||||
</ng-container>
|
|
||||||
<div [ngClass]="{'mt-4': plans[plan].noPayment}">
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
|
||||||
<span>{{'submit' | i18n}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
|
@ -3,243 +3,27 @@ import {
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { ActivatedRoute } from '@angular/router';
|
||||||
ActivatedRoute,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { OrganizationPlansComponent } from './organization-plans.component';
|
||||||
import { Angulartics2 } from 'angulartics2';
|
|
||||||
|
|
||||||
import { ApiService } from 'jslib/abstractions/api.service';
|
|
||||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
|
||||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
|
||||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
|
||||||
|
|
||||||
import { PaymentComponent } from './payment.component';
|
|
||||||
|
|
||||||
import { PlanType } from 'jslib/enums/planType';
|
|
||||||
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-organization',
|
selector: 'app-create-organization',
|
||||||
templateUrl: 'create-organization.component.html',
|
templateUrl: 'create-organization.component.html',
|
||||||
})
|
})
|
||||||
export class CreateOrganizationComponent implements OnInit {
|
export class CreateOrganizationComponent implements OnInit {
|
||||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
@ViewChild(OrganizationPlansComponent) orgPlansComponent: OrganizationPlansComponent;
|
||||||
|
|
||||||
selfHosted = false;
|
constructor(private route: ActivatedRoute) { }
|
||||||
ownedBusiness = false;
|
|
||||||
premiumAccessAddon = false;
|
|
||||||
storageGbPriceMonthly = 0.33;
|
|
||||||
additionalStorage = 0;
|
|
||||||
additionalSeats = 0;
|
|
||||||
plan = 'free';
|
|
||||||
interval = 'year';
|
|
||||||
name: string;
|
|
||||||
billingEmail: string;
|
|
||||||
businessName: string;
|
|
||||||
|
|
||||||
storageGb: any = {
|
|
||||||
price: 0.33,
|
|
||||||
monthlyPrice: 0.50,
|
|
||||||
yearlyPrice: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
plans: any = {
|
|
||||||
free: {
|
|
||||||
basePrice: 0,
|
|
||||||
noAdditionalSeats: true,
|
|
||||||
noPayment: true,
|
|
||||||
},
|
|
||||||
families: {
|
|
||||||
basePrice: 1,
|
|
||||||
annualBasePrice: 12,
|
|
||||||
baseSeats: 5,
|
|
||||||
noAdditionalSeats: true,
|
|
||||||
annualPlanType: PlanType.FamiliesAnnually,
|
|
||||||
canBuyPremiumAccessAddon: true,
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
basePrice: 5,
|
|
||||||
annualBasePrice: 60,
|
|
||||||
monthlyBasePrice: 8,
|
|
||||||
baseSeats: 5,
|
|
||||||
seatPrice: 2,
|
|
||||||
annualSeatPrice: 24,
|
|
||||||
monthlySeatPrice: 2.5,
|
|
||||||
monthPlanType: PlanType.TeamsMonthly,
|
|
||||||
annualPlanType: PlanType.TeamsAnnually,
|
|
||||||
},
|
|
||||||
enterprise: {
|
|
||||||
seatPrice: 3,
|
|
||||||
annualSeatPrice: 36,
|
|
||||||
monthlySeatPrice: 4,
|
|
||||||
monthPlanType: PlanType.EnterpriseMonthly,
|
|
||||||
annualPlanType: PlanType.EnterpriseAnnually,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
|
||||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
|
||||||
platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
|
|
||||||
private router: Router, private syncService: SyncService,
|
|
||||||
private route: ActivatedRoute) {
|
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||||
if (qParams.plan === 'families' || qParams.plan === 'teams' || qParams.plan === 'enterprise') {
|
if (qParams.plan === 'families' || qParams.plan === 'teams' || qParams.plan === 'enterprise') {
|
||||||
this.plan = qParams.plan;
|
this.orgPlansComponent.plan = qParams.plan;
|
||||||
}
|
}
|
||||||
if (queryParamsSub != null) {
|
if (queryParamsSub != null) {
|
||||||
queryParamsSub.unsubscribe();
|
queryParamsSub.unsubscribe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
|
||||||
let files: FileList = null;
|
|
||||||
if (this.selfHosted) {
|
|
||||||
const fileEl = document.getElementById('file') as HTMLInputElement;
|
|
||||||
files = fileEl.files;
|
|
||||||
if (files == null || files.length === 0) {
|
|
||||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
|
||||||
this.i18nService.t('selectFile'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let key: string = null;
|
|
||||||
let collectionCt: string = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.formPromise = this.cryptoService.makeShareKey().then((shareKey) => {
|
|
||||||
key = shareKey[0].encryptedString;
|
|
||||||
return this.cryptoService.encrypt(this.i18nService.t('defaultCollection'), shareKey[1]);
|
|
||||||
}).then((collection) => {
|
|
||||||
collectionCt = collection.encryptedString;
|
|
||||||
if (this.selfHosted || this.plan === 'free') {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return this.paymentComponent.createPaymentToken();
|
|
||||||
}
|
|
||||||
}).then((tokenResult) => {
|
|
||||||
if (this.selfHosted) {
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('license', files[0]);
|
|
||||||
fd.append('key', key);
|
|
||||||
fd.append('collectionName', collectionCt);
|
|
||||||
return this.apiService.postOrganizationLicense(fd);
|
|
||||||
} else {
|
|
||||||
const request = new OrganizationCreateRequest();
|
|
||||||
request.key = key;
|
|
||||||
request.collectionName = collectionCt;
|
|
||||||
request.name = this.name;
|
|
||||||
request.billingEmail = this.billingEmail;
|
|
||||||
|
|
||||||
if (this.plan === 'free') {
|
|
||||||
request.planType = PlanType.Free;
|
|
||||||
} else {
|
|
||||||
request.paymentToken = tokenResult[0];
|
|
||||||
request.paymentMethodType = tokenResult[1];
|
|
||||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
|
||||||
request.additionalSeats = this.additionalSeats;
|
|
||||||
request.additionalStorageGb = this.additionalStorage;
|
|
||||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
|
||||||
this.premiumAccessAddon;
|
|
||||||
if (this.interval === 'month') {
|
|
||||||
request.planType = this.plans[this.plan].monthPlanType;
|
|
||||||
} else {
|
|
||||||
request.planType = this.plans[this.plan].annualPlanType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.apiService.postOrganization(request);
|
|
||||||
}
|
|
||||||
}).then((response) => {
|
|
||||||
return this.finalize(response.id);
|
|
||||||
});
|
|
||||||
await this.formPromise;
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
async finalize(orgId: string) {
|
|
||||||
await this.apiService.refreshIdentityToken();
|
|
||||||
await this.syncService.fullSync(true);
|
|
||||||
this.analytics.eventTrack.next({ action: 'Created Organization' });
|
|
||||||
this.toasterService.popAsync('success', this.i18nService.t('organizationCreated'),
|
|
||||||
this.i18nService.t('organizationReadyToGo'));
|
|
||||||
this.router.navigate(['/organizations/' + orgId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
changedPlan() {
|
|
||||||
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
|
|
||||||
this.premiumAccessAddon = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.plans[this.plan].monthPlanType == null) {
|
|
||||||
this.interval = 'year';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.plans[this.plan].noAdditionalSeats) {
|
|
||||||
this.additionalSeats = 0;
|
|
||||||
} else if (!this.additionalSeats && !this.plans[this.plan].baseSeats &&
|
|
||||||
!this.plans[this.plan].noAdditionalSeats) {
|
|
||||||
this.additionalSeats = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changedOwnedBusiness() {
|
|
||||||
if (!this.ownedBusiness || this.plan === 'teams' || this.plan === 'enterprise') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.plan = 'teams';
|
|
||||||
}
|
|
||||||
|
|
||||||
additionalStorageTotal(annual: boolean): number {
|
|
||||||
if (annual) {
|
|
||||||
return (this.additionalStorage || 0) * this.storageGb.yearlyPrice;
|
|
||||||
} else {
|
|
||||||
return (this.additionalStorage || 0) * this.storageGb.monthlyPrice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seatTotal(annual: boolean): number {
|
|
||||||
if (this.plans[this.plan].noAdditionalSeats) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annual) {
|
|
||||||
return this.plans[this.plan].annualSeatPrice * (this.additionalSeats || 0);
|
|
||||||
} else {
|
|
||||||
return this.plans[this.plan].monthlySeatPrice * (this.additionalSeats || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
baseTotal(annual: boolean): number {
|
|
||||||
if (annual) {
|
|
||||||
return (this.plans[this.plan].annualBasePrice || 0);
|
|
||||||
} else {
|
|
||||||
return (this.plans[this.plan].monthlyBasePrice || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
premiumAccessTotal(annual: boolean): number {
|
|
||||||
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
|
|
||||||
if (annual) {
|
|
||||||
return 40;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get total(): number {
|
|
||||||
const annual = this.interval === 'year';
|
|
||||||
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
|
|
||||||
this.premiumAccessTotal(annual);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
<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="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||||
|
<span>{{'submit' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</ng-container>
|
||||||
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
||||||
|
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||||
|
<div class="row" *ngIf="createOrganization">
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<label for="name">{{'organizationName' | i18n}}</label>
|
||||||
|
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||||
|
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input id="ownedBusiness" class="form-check-input" type="checkbox" name="OwnedBusiness"
|
||||||
|
[(ngModel)]="ownedBusiness" (change)="changedOwnedBusiness()">
|
||||||
|
<label for="ownedBusiness" class="form-check-label">{{'accountOwnedBusiness' | i18n}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="row" *ngIf="ownedBusiness">
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||||
|
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="businessName">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
|
||||||
|
<div class="form-check form-check-block" *ngIf="!ownedBusiness && showFree">
|
||||||
|
<input class="form-check-input" type="radio" name="PlanType" id="planFree" value="free" [(ngModel)]="plan"
|
||||||
|
(change)="changedPlan()">
|
||||||
|
<label class="form-check-label" for="planFree">
|
||||||
|
{{'planNameFree' | i18n}}
|
||||||
|
<small class="mb-1">{{'planDescFree' | i18n : '1'}}</small>
|
||||||
|
<small>• {{'limitedUsers' | i18n : '2'}}</small>
|
||||||
|
<small>• {{'limitedCollections' | i18n : '2'}}</small>
|
||||||
|
<span>{{'freeForever' | i18n}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
|
||||||
|
<input class="form-check-input" type="radio" name="PlanType" id="planFamilies" value="families"
|
||||||
|
[(ngModel)]="plan" (change)="changedPlan()">
|
||||||
|
<label class="form-check-label" for="planFamilies">
|
||||||
|
{{'planNameFamilies' | i18n}}
|
||||||
|
<small class="mb-1">{{'planDescFamilies' | i18n}}</small>
|
||||||
|
<small>• {{'addShareLimitedUsers' | i18n : '5'}}</small>
|
||||||
|
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||||
|
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||||
|
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
||||||
|
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||||
|
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||||
|
<span>{{1 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-block">
|
||||||
|
<input class="form-check-input" type="radio" name="PlanType" id="planTeams" value="teams" [(ngModel)]="plan"
|
||||||
|
(change)="changedPlan()">
|
||||||
|
<label class="form-check-label" for="planTeams">
|
||||||
|
{{'planNameTeams' | i18n}}
|
||||||
|
<small class="mb-1">{{'planDescTeams' | i18n}}</small>
|
||||||
|
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
||||||
|
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||||
|
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||||
|
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||||
|
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||||
|
<span>{{5 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}},
|
||||||
|
{{('additionalUsers' | i18n).toLowerCase()}}
|
||||||
|
{{2 | currency:'$'}} /{{'month' | i18n}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-block">
|
||||||
|
<input class="form-check-input" type="radio" name="PlanType" id="planEnterprise" value="enterprise"
|
||||||
|
[(ngModel)]="plan" (change)="changedPlan()">
|
||||||
|
<label class="form-check-label" for="planEnterprise">
|
||||||
|
{{'planNameEnterprise' | i18n}}
|
||||||
|
<small class="mb-1">{{'planDescEnterprise' | i18n}}</small>
|
||||||
|
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
||||||
|
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||||
|
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||||
|
<small>• {{'controlAccessWithGroups' | i18n}}</small>
|
||||||
|
<small>• {{'trackAuditLogs' | i18n}}</small>
|
||||||
|
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
|
||||||
|
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
||||||
|
<small>• {{'usersGetPremium' | i18n}}</small>
|
||||||
|
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||||
|
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||||
|
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!plans[plan].noPayment">
|
||||||
|
<ng-container *ngIf="!plans[plan].noAdditionalSeats && !plans[plan].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"
|
||||||
|
[(ngModel)]="additionalSeats" min="1" max="100000" 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="!plans[plan].noAdditionalSeats && plans[plan].baseSeats">
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<label for="additionalSeats">{{'additionalUserSeats' | i18n}}</label>
|
||||||
|
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
|
||||||
|
[(ngModel)]="additionalSeats" min="0" max="100000" placeholder="{{'userSeatsDesc' | i18n}}">
|
||||||
|
<small
|
||||||
|
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | 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"
|
||||||
|
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
|
||||||
|
placeholder="{{'additionalStorageGbDesc' | i18n}}">
|
||||||
|
<small
|
||||||
|
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
|
||||||
|
[(ngModel)]="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">
|
||||||
|
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalAnnually" value="year"
|
||||||
|
[(ngModel)]="interval">
|
||||||
|
<label class="form-check-label" for="intervalAnnually">
|
||||||
|
{{'annually' | i18n}}
|
||||||
|
<small *ngIf="plans[plan].annualBasePrice">
|
||||||
|
{{'basePrice' | i18n}}: {{plans[plan].basePrice | currency:'$'}} ×12 {{'monthAbbr' | i18n}} =
|
||||||
|
{{baseTotal(true) | currency:'$'}}
|
||||||
|
/{{'year' | i18n}}
|
||||||
|
</small>
|
||||||
|
<small *ngIf="!plans[plan].noAdditionalSeats">
|
||||||
|
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||||
|
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
||||||
|
{{additionalSeats || 0}} × {{plans[plan].seatPrice | currency:'$'}} ×12
|
||||||
|
{{'monthAbbr' | i18n}} = {{seatTotal(true)
|
||||||
|
| currency:'$'}} /{{'year' | i18n}}
|
||||||
|
</small>
|
||||||
|
<small>
|
||||||
|
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||||
|
{{storageGb.price | currency:'$'}} ×12 {{'monthAbbr'
|
||||||
|
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
|
||||||
|
</small>
|
||||||
|
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
|
||||||
|
{{'premiumAccess' | i18n}}:
|
||||||
|
{{3.33 | currency:'$'}} ×12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">
|
||||||
|
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalMonthly" value="month"
|
||||||
|
[(ngModel)]="interval">
|
||||||
|
<label class="form-check-label" for="intervalMonthly">
|
||||||
|
{{'monthly' | i18n}}
|
||||||
|
<small *ngIf="plans[plan].monthlyBasePrice">
|
||||||
|
{{'basePrice' | i18n}}: {{baseTotal(false) | currency:'$'}} /{{'month' | i18n}}
|
||||||
|
</small>
|
||||||
|
<small *ngIf="!plans[plan].noAdditionalSeats">
|
||||||
|
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||||
|
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
||||||
|
{{additionalSeats || 0}} × {{plans[plan].monthlySeatPrice | currency:'$'}} =
|
||||||
|
{{seatTotal(false) | currency:'$'}} /{{'month'
|
||||||
|
| i18n}}
|
||||||
|
</small>
|
||||||
|
<small>
|
||||||
|
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||||
|
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
|
||||||
|
| currency:'$'}} /{{'month' | i18n}}
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<hr class="my-3">
|
||||||
|
<div class="text-lg">
|
||||||
|
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted font-italic"
|
||||||
|
*ngIf="createOrganization">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
|
||||||
|
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
|
||||||
|
<app-payment [hideCredit]="true" [hideBank]="!!organizationId"></app-payment>
|
||||||
|
</ng-container>
|
||||||
|
<div [ngClass]="{'mt-4': plans[plan].noPayment}">
|
||||||
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||||
|
<span>{{'submit' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
||||||
|
{{'cancel' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -0,0 +1,257 @@
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
import { Angulartics2 } from 'angulartics2';
|
||||||
|
|
||||||
|
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
|
||||||
|
|
||||||
|
import { ApiService } from 'jslib/abstractions/api.service';
|
||||||
|
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||||
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||||
|
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||||
|
|
||||||
|
import { PaymentComponent } from './payment.component';
|
||||||
|
|
||||||
|
import { PlanType } from 'jslib/enums/planType';
|
||||||
|
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-organization-plans',
|
||||||
|
templateUrl: 'organization-plans.component.html',
|
||||||
|
})
|
||||||
|
export class OrganizationPlansComponent {
|
||||||
|
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||||
|
|
||||||
|
@Input() organizationId: string;
|
||||||
|
@Input() showFree = true;
|
||||||
|
@Input() showCancel = false;
|
||||||
|
@Input() plan = 'free';
|
||||||
|
@Output() onSuccess = new EventEmitter();
|
||||||
|
@Output() onCanceled = new EventEmitter();
|
||||||
|
|
||||||
|
selfHosted = false;
|
||||||
|
ownedBusiness = false;
|
||||||
|
premiumAccessAddon = false;
|
||||||
|
storageGbPriceMonthly = 0.33;
|
||||||
|
additionalStorage = 0;
|
||||||
|
additionalSeats = 0;
|
||||||
|
interval = 'year';
|
||||||
|
name: string;
|
||||||
|
billingEmail: string;
|
||||||
|
businessName: string;
|
||||||
|
|
||||||
|
storageGb: any = {
|
||||||
|
price: 0.33,
|
||||||
|
monthlyPrice: 0.50,
|
||||||
|
yearlyPrice: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
plans: any = {
|
||||||
|
free: {
|
||||||
|
basePrice: 0,
|
||||||
|
noAdditionalSeats: true,
|
||||||
|
noPayment: true,
|
||||||
|
},
|
||||||
|
families: {
|
||||||
|
basePrice: 1,
|
||||||
|
annualBasePrice: 12,
|
||||||
|
baseSeats: 5,
|
||||||
|
noAdditionalSeats: true,
|
||||||
|
annualPlanType: PlanType.FamiliesAnnually,
|
||||||
|
canBuyPremiumAccessAddon: true,
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
basePrice: 5,
|
||||||
|
annualBasePrice: 60,
|
||||||
|
monthlyBasePrice: 8,
|
||||||
|
baseSeats: 5,
|
||||||
|
seatPrice: 2,
|
||||||
|
annualSeatPrice: 24,
|
||||||
|
monthlySeatPrice: 2.5,
|
||||||
|
monthPlanType: PlanType.TeamsMonthly,
|
||||||
|
annualPlanType: PlanType.TeamsAnnually,
|
||||||
|
},
|
||||||
|
enterprise: {
|
||||||
|
seatPrice: 3,
|
||||||
|
annualSeatPrice: 36,
|
||||||
|
monthlySeatPrice: 4,
|
||||||
|
monthPlanType: PlanType.EnterpriseMonthly,
|
||||||
|
annualPlanType: PlanType.EnterpriseAnnually,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
|
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||||
|
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||||
|
platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
|
||||||
|
private router: Router, private syncService: SyncService) {
|
||||||
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
let files: FileList = null;
|
||||||
|
if (this.createOrganization && this.selfHosted) {
|
||||||
|
const fileEl = document.getElementById('file') as HTMLInputElement;
|
||||||
|
files = fileEl.files;
|
||||||
|
if (files == null || files.length === 0) {
|
||||||
|
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||||
|
this.i18nService.t('selectFile'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.doSubmit(files);
|
||||||
|
await this.formPromise;
|
||||||
|
this.onSuccess.emit();
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.onCanceled.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
changedPlan() {
|
||||||
|
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
|
||||||
|
this.premiumAccessAddon = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.plans[this.plan].monthPlanType == null) {
|
||||||
|
this.interval = 'year';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.plans[this.plan].noAdditionalSeats) {
|
||||||
|
this.additionalSeats = 0;
|
||||||
|
} else if (!this.additionalSeats && !this.plans[this.plan].baseSeats &&
|
||||||
|
!this.plans[this.plan].noAdditionalSeats) {
|
||||||
|
this.additionalSeats = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changedOwnedBusiness() {
|
||||||
|
if (!this.ownedBusiness || this.plan === 'teams' || this.plan === 'enterprise') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.plan = 'teams';
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalStorageTotal(annual: boolean): number {
|
||||||
|
if (annual) {
|
||||||
|
return Math.abs(this.additionalStorage || 0) * this.storageGb.yearlyPrice;
|
||||||
|
} else {
|
||||||
|
return Math.abs(this.additionalStorage || 0) * this.storageGb.monthlyPrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seatTotal(annual: boolean): number {
|
||||||
|
if (this.plans[this.plan].noAdditionalSeats) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annual) {
|
||||||
|
return this.plans[this.plan].annualSeatPrice * Math.abs(this.additionalSeats || 0);
|
||||||
|
} else {
|
||||||
|
return this.plans[this.plan].monthlySeatPrice * Math.abs(this.additionalSeats || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseTotal(annual: boolean): number {
|
||||||
|
if (annual) {
|
||||||
|
return Math.abs(this.plans[this.plan].annualBasePrice || 0);
|
||||||
|
} else {
|
||||||
|
return Math.abs(this.plans[this.plan].monthlyBasePrice || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
premiumAccessTotal(annual: boolean): number {
|
||||||
|
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
|
||||||
|
if (annual) {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get total(): number {
|
||||||
|
const annual = this.interval === 'year';
|
||||||
|
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
|
||||||
|
this.premiumAccessTotal(annual);
|
||||||
|
}
|
||||||
|
|
||||||
|
get createOrganization() {
|
||||||
|
return this.organizationId == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doSubmit(files: FileList) {
|
||||||
|
let tokenResult: [string, PaymentMethodType] = null;
|
||||||
|
if (!this.selfHosted && this.plan !== 'free') {
|
||||||
|
tokenResult = await this.paymentComponent.createPaymentToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (this.selfHosted) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('license', files[0]);
|
||||||
|
fd.append('key', key);
|
||||||
|
fd.append('collectionName', collectionCt);
|
||||||
|
const response = await this.apiService.postOrganizationLicense(fd);
|
||||||
|
orgId = response.id;
|
||||||
|
} else {
|
||||||
|
const request = new OrganizationCreateRequest();
|
||||||
|
request.key = key;
|
||||||
|
request.collectionName = collectionCt;
|
||||||
|
request.name = this.name;
|
||||||
|
request.billingEmail = this.billingEmail;
|
||||||
|
|
||||||
|
if (this.plan === 'free') {
|
||||||
|
request.planType = PlanType.Free;
|
||||||
|
} else {
|
||||||
|
request.paymentToken = tokenResult[0];
|
||||||
|
request.paymentMethodType = tokenResult[1];
|
||||||
|
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||||
|
request.additionalSeats = this.additionalSeats;
|
||||||
|
request.additionalStorageGb = this.additionalStorage;
|
||||||
|
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||||
|
this.premiumAccessAddon;
|
||||||
|
if (this.interval === 'month') {
|
||||||
|
request.planType = this.plans[this.plan].monthPlanType;
|
||||||
|
} else {
|
||||||
|
request.planType = this.plans[this.plan].annualPlanType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await this.apiService.postOrganization(request);
|
||||||
|
orgId = response.id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
orgId = this.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orgId != null) {
|
||||||
|
await this.apiService.refreshIdentityToken();
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
this.analytics.eventTrack.next({
|
||||||
|
action: (this.createOrganization ? 'Created' : 'Upgraded') + ' Organization',
|
||||||
|
});
|
||||||
|
this.toasterService.popAsync('success',
|
||||||
|
this.i18nService.t(this.createOrganization ? 'organizationCreated' : ''), // TODO
|
||||||
|
this.i18nService.t('organizationReadyToGo'));
|
||||||
|
this.router.navigate(['/organizations/' + orgId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -102,7 +102,7 @@ export class PremiumComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
get additionalStorageTotal(): number {
|
get additionalStorageTotal(): number {
|
||||||
return this.storageGbPrice * this.additionalStorage;
|
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get total(): number {
|
get total(): number {
|
||||||
|
|
|
@ -28,7 +28,7 @@ $h5-font-size: 1rem;
|
||||||
$h6-font-size: 1rem;
|
$h6-font-size: 1rem;
|
||||||
|
|
||||||
$small-font-size: 90%;
|
$small-font-size: 90%;
|
||||||
$font-size-lg: 1.18rem;
|
$font-size-lg: 1.15rem;
|
||||||
$code-font-size: 100%;
|
$code-font-size: 100%;
|
||||||
|
|
||||||
$navbar-padding-y: .75rem;
|
$navbar-padding-y: .75rem;
|
||||||
|
@ -185,7 +185,7 @@ input, select, textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body-header {
|
.card-body-header {
|
||||||
font-size: $h3-font-size * 1.12;
|
font-size: $font-size-lg;
|
||||||
@extend .mb-4
|
@extend .mb-4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,6 +216,12 @@ input, select, textarea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-org-plans {
|
||||||
|
h2 {
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
width: $modal-md;
|
width: $modal-md;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue