org create

This commit is contained in:
Kyle Spearrin 2018-07-02 17:09:53 -04:00
parent bd070ff066
commit 1f62b9fdcb
9 changed files with 604 additions and 9 deletions

2
jslib

@ -1 +1 @@
Subproject commit 8be95bfe574a7ae2c8173921bbdfe82451436081
Subproject commit e22915818cb7c6a756c4ac34124b14e33621c5aa

View File

@ -15,6 +15,7 @@ import { RegisterComponent } from './accounts/register.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { AccountComponent } from './settings/account.component';
import { CreateOrganizationComponent } from './settings/create-organization.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
import { OptionsComponent } from './settings/options.component';
import { SettingsComponent } from './settings/settings.component';
@ -60,6 +61,11 @@ const routes: Routes = [
{ path: 'domain-rules', component: DomainRulesComponent, canActivate: [AuthGuardService] },
{ path: 'two-factor', component: TwoFactorSetupComponent, canActivate: [AuthGuardService] },
{ path: 'billing', component: UserBillingComponent, canActivate: [AuthGuardService] },
{
path: 'create-organization',
component: CreateOrganizationComponent,
canActivate: [AuthGuardService],
},
],
},
{

View File

@ -37,6 +37,7 @@ import { AdjustPaymentComponent } from './settings/adjust-payment.component';
import { AdjustStorageComponent } from './settings/adjust-storage.component';
import { ChangeEmailComponent } from './settings/change-email.component';
import { ChangePasswordComponent } from './settings/change-password.component';
import { CreateOrganizationComponent } from './settings/create-organization.component';
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
import { DeleteAccountComponent } from './settings/delete-account.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
@ -127,6 +128,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
ChangePasswordComponent,
CiphersComponent,
CollectionsComponent,
CreateOrganizationComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DomainRulesComponent,

View File

@ -0,0 +1,169 @@
<div class="page-header">
<h1>{{'newOrganization' | i18n}}</h1>
</div>
<p>{{'newOrganizationDesc' | i18n}}</p>
<ng-container *ngIf="selfHosted">
<p>{{'uploadLicenseFilePremium' | i18n}}</p>
<app-update-license [user]="true" [create]="true" (onUpdated)="finalizePremium()"></app-update-license>
</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>• {{'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">{{'additionalStorageDesc' | i18n : '1 GB' : (storageGb.price | currency:'$')}}</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:'$'}} &times;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}} &times; {{plans[plan].seatPrice | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} = {{seatTotal(true)
| currency:'$'}} /{{'year' | i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times; {{storageGb.price | currency:'$'}} &times;12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(true) | 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}} &times; {{plans[plan].monthlySeatPrice | currency:'$'}} = {{seatTotal(false) | currency:'$'}} /{{'month'
| i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times; {{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
| currency:'$'}} /{{'month' | i18n}}
</small>
</label>
</div>
<hr class="my-2">
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
<br>
<small class="text-muted">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment></app-payment>
</ng-container>
<div [ngClass]="{'mt-4': plans[plan].noPayment}">
<button type="submit" class="btn btn-primary btn-submit" appBlurClick [disabled]="form.loading">
<i class="fa fa-spinner fa-spin"></i>
<span>{{'submit' | i18n}}</span>
</button>
</div>
</form>

View File

@ -0,0 +1,194 @@
import {
Component,
EventEmitter,
Output,
ViewChild,
} from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
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 { PaymentComponent } from './payment.component';
import { PlanType } from 'jslib/enums/planType';
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
@Component({
selector: 'app-create-organization',
templateUrl: 'create-organization.component.html',
})
export class CreateOrganizationComponent {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
selfHosted = false;
ownedBusiness = 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,
},
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) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async submit() {
let key: string = null;
let collectionCt: string = null;
try {
this.formPromise = this.cryptoService.makeShareKey().then((shareKey) => {
key = shareKey[0].encryptedString;
return this.cryptoService.encrypt('Default Collection', shareKey[1]);
}).then((collection) => {
collectionCt = collection.encryptedString;
if (this.plan === 'free') {
return null;
} else {
return this.paymentComponent.createPaymentToken();
}
}).then((token: string) => {
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 = token;
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.country = this.paymentComponent.getCountry();
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) {
this.apiService.refreshIdentityToken();
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].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);
}
}
get total(): number {
const annual = this.interval === 'year';
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual);
}
}

View File

@ -39,7 +39,7 @@
<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">{{'additionalStorageDesc' | i18n : (storageGbPrice | currency:'$')}}</small>
<small class="text-muted form-text">{{'additionalStorageDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$')}}</small>
</div>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>

View File

@ -10,7 +10,7 @@
</ul>
<p *ngIf="!organizations || !organizations.length">{{'noOrganizationsList' | i18n}}</p>
</ng-container>
<button (click)="newOrganization()" class="btn btn-block btn-outline-primary">
<a href="#" appStopClick routerLink="/settings/create-organization" class="btn btn-block btn-outline-primary">
<i class="fa fa-plus fa-fw"></i>
{{'newOrganization' | i18n}}
</button>
</a>

View File

@ -570,7 +570,7 @@
"message": "New Organization"
},
"noOrganizationsList": {
"message": "You do not belong to any organizations."
"message": "You do not belong to any organizations. Organizations allow you to securely share items with other users."
},
"versionNumber": {
"message": "Version $VERSION_NUMBER$",
@ -1254,10 +1254,14 @@
"message": "# of additional GB"
},
"additionalStorageDesc": {
"message": "Your plan comes with 1 GB of encrypted file storage. You can add additional storage for $PRICE$ per GB /year.",
"message": "Your plan comes with $SIZE$ of encrypted file storage. You can add additional storage for $PRICE$ per GB /year.",
"placeholders": {
"price": {
"size": {
"content": "$1",
"example": "1 GB"
},
"price": {
"content": "$2",
"example": "$4.00"
}
}
@ -1274,11 +1278,21 @@
"month": {
"message": "month"
},
"monthAbbr": {
"message": "mo.",
"description": "Short abbreviation for 'month'"
},
"paymentChargedAnnually": {
"message": "Your payment method will be charged immediately and on a recurring basis each year. You may cancel at any time."
},
"paymentChargedMonthly": {
"message": "Your payment method will be charged immediately and on a recurring basis each month. You may cancel at any time."
"paymentChargedWithTrial": {
"message": "Your plan comes with a free 7 day trial. Your card will not be charged until the trial has ended and on a recurring basis each $INTERVAL$. You may cancel at any time.",
"placeholders": {
"interval": {
"content": "$1",
"example": "year"
}
}
},
"paymentInformation": {
"message": "Payment Information"
@ -1440,5 +1454,192 @@
},
"accountEmailMustBeVerified": {
"message": "Your account's email address must be verified."
},
"newOrganizationDesc": {
"message": "Organizations allow you to share parts of your vault with others as well as manage related users for a specific entity such as a family, small team, or large company."
},
"generalInformation": {
"message": "General Information"
},
"organizationName": {
"message": "Organization Name"
},
"accountOwnedBusiness": {
"message": "This account is owned by a business."
},
"billingEmail": {
"message": "Billing Email"
},
"businessName": {
"message": "Business Name"
},
"chooseYourPlan": {
"message": "Choose Your Plan"
},
"users": {
"message": "Users"
},
"userSeats": {
"message": "User Seats"
},
"additionalUserSeats": {
"message": "Additional User Seats"
},
"userSeatsDesc": {
"message": "# of user seats"
},
"userSeatsAdditionalDesc": {
"message": "Your plan comes with $BASE_SEATS$ user seats. You can add additional users for $SEAT_PRICE$ per user /month.",
"placeholders": {
"base_seats": {
"content": "$1",
"example": "5"
},
"seat_price": {
"content": "$2",
"example": "$2.00"
}
}
},
"userSeatsHowManyDesc": {
"message": "How many user seats do you need? You can also add additional seats later if needed."
},
"planNameFree": {
"message": "Free"
},
"planDescFree": {
"message": "For testing or personal users to share with $COUNT$ other user.",
"placeholders": {
"count": {
"content": "$1",
"example": "1"
}
}
},
"planNameFamilies": {
"message": "Families"
},
"planDescFamilies": {
"message": "For personal use, to share with family & friends."
},
"planNameTeams": {
"message": "Teams"
},
"planDescTeams": {
"message": "For businesses and other team organizations."
},
"planNameEnterprise": {
"message": "Enterprise"
},
"planDescEnterprise": {
"message": "For businesses and other large organizations."
},
"freeForever": {
"message": "Free Forever"
},
"includesXUsers": {
"message": "includes $COUNT$ users",
"placeholders": {
"count": {
"content": "$1",
"example": "5"
}
}
},
"additionalUsers": {
"message": "Additional Users"
},
"costPerUser": {
"message": "$COST$ per user",
"placeholders": {
"cost": {
"content": "$1",
"example": "$3"
}
}
},
"limitedUsers": {
"message": "Limited to $COUNT$ users (including you)",
"placeholders": {
"count": {
"content": "$1",
"example": "2"
}
}
},
"limitedCollections": {
"message": "Limited to $COUNT$ collections",
"placeholders": {
"count": {
"content": "$1",
"example": "2"
}
}
},
"addShareLimitedUsers": {
"message": "Add and share with up to $COUNT$ users",
"placeholders": {
"count": {
"content": "$1",
"example": "5"
}
}
},
"addShareUnlimitedUsers": {
"message": "Add and share with unlimited users"
},
"createUnlimitedCollections": {
"message": "Create unlimited collections"
},
"gbEncryptedFileStorage": {
"message": "$SIZE$ encrypted file storage",
"placeholders": {
"size": {
"content": "$1",
"example": "1 GB"
}
}
},
"onPremHostingOptional": {
"message": "On-premise hosting (optional)"
},
"controlAccessWithGroups": {
"message": "Control user access with groups"
},
"syncUsersFromDirectory": {
"message": "Sync your users and groups from a directory"
},
"trackAuditLogs": {
"message": "Track user actions with audit logs"
},
"enforce2faDuo": {
"message": "Enforce 2FA with Duo"
},
"priorityCustomerSupport": {
"message": "Priority customer support"
},
"xDayFreeTrial": {
"message": "$COUNT$ day free trial, cancel anytime",
"placeholders": {
"count": {
"content": "$1",
"example": "7"
}
}
},
"monthly": {
"message": "Monthly"
},
"annually": {
"message": "Annually"
},
"basePrice": {
"message": "Base Price"
},
"organizationCreated": {
"message": "Organization Created"
},
"organizationReadyToGo": {
"message": "Your new organization is ready to go!"
}
}

View File

@ -25,6 +25,7 @@ $h4-font-size: 1rem;
$h5-font-size: 1rem;
$h6-font-size: 1rem;
$small-font-size: 90%;
$font-size-lg: 1.18rem;
$code-font-size: 100%;
@ -548,3 +549,25 @@ app-user-billing {
min-width: 100px;
}
}
.form-check-block {
.form-check-label {
font-weight: bold;
> small {
display: block;
color: $text-muted;
font-weight: normal;
}
> span {
display: block;
font-weight: normal;
@extend .mt-2;
}
}
}
.form-check-block + .form-check-block {
@extend .mt-3;
}