From 1f62b9fdcb1e9c07773885afd08c7a67ae6c14cb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 17:09:53 -0400 Subject: [PATCH] org create --- jslib | 2 +- src/app/app-routing.module.ts | 6 + src/app/app.module.ts | 2 + .../create-organization.component.html | 169 ++++++++++++++ .../settings/create-organization.component.ts | 194 ++++++++++++++++ src/app/settings/premium.component.html | 2 +- src/app/vault/organizations.component.html | 4 +- src/locales/en/messages.json | 211 +++++++++++++++++- src/scss/styles.scss | 23 ++ 9 files changed, 604 insertions(+), 9 deletions(-) create mode 100644 src/app/settings/create-organization.component.html create mode 100644 src/app/settings/create-organization.component.ts diff --git a/jslib b/jslib index 8be95bfe57..e22915818c 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 8be95bfe574a7ae2c8173921bbdfe82451436081 +Subproject commit e22915818cb7c6a756c4ac34124b14e33621c5aa diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index dcf8d1bfff..a512963d1d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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], + }, ], }, { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1466208961..6ba5a85a4b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/settings/create-organization.component.html b/src/app/settings/create-organization.component.html new file mode 100644 index 0000000000..efcf397689 --- /dev/null +++ b/src/app/settings/create-organization.component.html @@ -0,0 +1,169 @@ + +

{{'newOrganizationDesc' | i18n}}

+ +

{{'uploadLicenseFilePremium' | i18n}}

+ +
+
+

{{'generalInformation' | i18n}}

+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+

{{'chooseYourPlan' | i18n}}

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +

{{'users' | i18n}}

+
+
+ + + {{'userSeatsHowManyDesc' | i18n}} +
+
+
+

{{'addons' | i18n}}

+
+
+ + + {{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | currency:'$')}} +
+
+
+
+ + + {{'additionalStorageDesc' | i18n : '1 GB' : (storageGb.price | currency:'$')}} +
+
+

{{'summary' | i18n}}

+
+ + +
+
+ + +
+
+ {{'total' | i18n}}: {{total | currency:'USD $'}} /{{interval | i18n}} +
+ {{'paymentChargedWithTrial' | i18n : (interval | i18n) }} +

{{'paymentInformation' | i18n}}

+ +
+
+ +
+
diff --git a/src/app/settings/create-organization.component.ts b/src/app/settings/create-organization.component.ts new file mode 100644 index 0000000000..132e2d34e9 --- /dev/null +++ b/src/app/settings/create-organization.component.ts @@ -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; + + 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); + } +} diff --git a/src/app/settings/premium.component.html b/src/app/settings/premium.component.html index e7f5ea6365..7992ad4320 100644 --- a/src/app/settings/premium.component.html +++ b/src/app/settings/premium.component.html @@ -39,7 +39,7 @@ - {{'additionalStorageDesc' | i18n : (storageGbPrice | currency:'$')}} + {{'additionalStorageDesc' | i18n : '1 GB' : (storageGbPrice | currency:'$')}}

{{'summary' | i18n}}

diff --git a/src/app/vault/organizations.component.html b/src/app/vault/organizations.component.html index a5f1304b9d..202d0b69a2 100644 --- a/src/app/vault/organizations.component.html +++ b/src/app/vault/organizations.component.html @@ -10,7 +10,7 @@

{{'noOrganizationsList' | i18n}}

- + diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 6b8a8e2148..f70af4e25b 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -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!" } } diff --git a/src/scss/styles.scss b/src/scss/styles.scss index e0e5091006..bc9418d15b 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -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; +}