PM-5017 Migrate Organization Plans component (#8448)

* PM-5017 Migrated Organization plans component

* PM-5017 Addressed all the review comments

* PM-5017 Missed a minor change

---------

Co-authored-by: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
This commit is contained in:
KiruthigaManivannan 2024-05-09 21:11:17 +05:30 committed by GitHub
parent 0c2e8c15dc
commit ff3b6f52ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 444 additions and 407 deletions

View File

@ -1,400 +1,428 @@
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<i <i
class="bwi bwi-spinner bwi-spin text-muted" class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<ng-container *ngIf="createOrganization && selfHosted"> <ng-container *ngIf="createOrganization && selfHosted">
<p>{{ "uploadLicenseFileOrg" | i18n }}</p> <p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> <form [formGroup]="selfHostedForm" [bitSubmit]="submit">
<div class="form-group"> <bit-form-field>
<label for="file">{{ "licenseFile" | i18n }}</label> <bit-label>{{ "licenseFile" | i18n }}</bit-label>
<input type="file" id="file" class="form-control-file" name="file" required /> <div>
<small class="form-text text-muted">{{ <button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
"licenseFileDesc" | i18n: "bitwarden_organization_license.json" {{ "chooseFile" | i18n }}
}}</small> </button>
</div> {{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> </div>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <input
<span>{{ "submit" | i18n }}</span> #fileSelector
hidden
bitInput
type="file"
formControlName="file"
(change)="setSelectedFile($event)"
accept="application/JSON"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button> </button>
</form> </form>
</ng-container> </ng-container>
<form <form
#form
[formGroup]="formGroup" [formGroup]="formGroup"
(ngSubmit)="submit()" [bitSubmit]="submit"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans" *ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
class="tw-pt-6" class="tw-pt-6"
> >
<app-org-info <bit-section>
(changedBusinessOwned)="changedOwnedBusiness()" <app-org-info
[formGroup]="formGroup" (changedBusinessOwned)="changedOwnedBusiness()"
[createOrganization]="createOrganization" [formGroup]="formGroup"
[isProvider]="!!providerId" [createOrganization]="createOrganization"
[acceptingSponsorship]="acceptingSponsorship" [isProvider]="!!providerId"
></app-org-info> [acceptingSponsorship]="acceptingSponsorship"
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2> >
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block"> </app-org-info>
<input </bit-section>
class="form-check-input" <bit-section>
type="radio" <h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
name="product" <div *ngFor="let selectableProduct of selectableProducts">
id="product{{ selectableProduct.product }}" <bit-radio-group formControlName="product" [block]="true">
[value]="selectableProduct.product" <bit-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
formControlName="product" <bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
(change)="changedProduct()" <bit-hint class="tw-text-sm"
/> >{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}
<label class="form-check-label" for="product{{ selectableProduct.product }}"> <ng-container
{{ selectableProduct.nameLocalizationKey | i18n }} *ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans"
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}</small> >
<ng-container <ul class="tw-pl-0 tw-list-inside tw-mb-0">
*ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans" <li>{{ "includeAllTeamsFeatures" | i18n }}</li>
> <li *ngIf="selectableProduct.hasSelfHost">{{ "onPremHostingOptional" | i18n }}</li>
<small>• {{ "includeAllTeamsFeatures" | i18n }}</small> <li *ngIf="selectableProduct.hasSso">{{ "includeSsoAuthentication" | i18n }}</li>
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small> <li *ngIf="selectableProduct.hasPolicies">
<small *ngIf="selectableProduct.hasSso">• {{ "includeSsoAuthentication" | i18n }}</small> {{ "includeEnterprisePolicies" | i18n }}
<small *ngIf="selectableProduct.hasPolicies" </li>
>• {{ "includeEnterprisePolicies" | i18n }}</small <li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
> {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization" </li>
>• </ul>
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }} </ng-container>
</small> <ng-template #nonEnterprisePlans>
</ng-container> <ng-container
<ng-template #nonEnterprisePlans> *ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList"
<ng-container >
*ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList" <ul class="tw-pl-0 tw-list-inside tw-mb-0">
> <li>{{ "includeAllTeamsStarterFeatures" | i18n }}</li>
<small>• {{ "includeAllTeamsStarterFeatures" | i18n }}</small> <li>{{ "chooseMonthlyOrAnnualBilling" | i18n }}</li>
<small>• {{ "chooseMonthlyOrAnnualBilling" | i18n }}</small> <li>{{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</li>
<small>• {{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</small> <li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"> {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
• {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }} </li>
</small> </ul>
</ng-container> </ng-container>
<ng-template #fullFeatureList> <ng-template #fullFeatureList>
<small *ngIf="selectableProduct.product == productTypes.Free" <ul class="tw-pl-0 tw-list-inside tw-mb-0">
>• {{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small <li *ngIf="selectableProduct.product == productTypes.Free">
{{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
</li>
<li
*ngIf="
selectableProduct.product != productTypes.Free &&
selectableProduct.product != productTypes.TeamsStarter &&
selectableProduct.PasswordManager.maxSeats
"
>
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
</li>
<li *ngIf="!selectableProduct.PasswordManager.maxSeats">
{{ "addShareUnlimitedUsers" | i18n }}
</li>
<li *ngIf="selectableProduct.PasswordManager.maxCollections">
{{
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
}}
</li>
<li *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats">
{{
"addShareLimitedUsers"
| i18n: selectableProduct.PasswordManager.maxAdditionalSeats
}}
</li>
<li *ngIf="!selectableProduct.PasswordManager.maxCollections">
{{ "createUnlimitedCollections" | i18n }}
</li>
<li *ngIf="selectableProduct.PasswordManager.baseStorageGb">
{{
"gbEncryptedFileStorage"
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
}}
</li>
<li *ngIf="selectableProduct.hasGroups">
{{ "controlAccessWithGroups" | i18n }}
</li>
<li *ngIf="selectableProduct.hasApi">{{ "trackAuditLogs" | i18n }}</li>
<li *ngIf="selectableProduct.hasDirectory">
{{ "syncUsersFromDirectory" | i18n }}
</li>
<li *ngIf="selectableProduct.hasSelfHost">
{{ "onPremHostingOptional" | i18n }}
</li>
<li *ngIf="selectableProduct.usersGetPremium">{{ "usersGetPremium" | i18n }}</li>
<li *ngIf="selectableProduct.product != productTypes.Free">
{{ "priorityCustomerSupport" | i18n }}
</li>
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
</li>
</ul>
</ng-template>
</ng-template>
</bit-hint>
</bit-radio-button>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
> >
<small
*ngIf="
selectableProduct.product != productTypes.Free &&
selectableProduct.product != productTypes.TeamsStarter &&
selectableProduct.PasswordManager.maxSeats
"
>•
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small
>
<small *ngIf="!selectableProduct.PasswordManager.maxSeats"
>• {{ "addShareUnlimitedUsers" | i18n }}</small
>
<small *ngIf="selectableProduct.PasswordManager.maxCollections"
>•
{{
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
}}</small
>
<small *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats"
>•
{{
"addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxAdditionalSeats
}}</small
>
<small *ngIf="!selectableProduct.PasswordManager.maxCollections"
>• {{ "createUnlimitedCollections" | i18n }}</small
>
<small *ngIf="selectableProduct.PasswordManager.baseStorageGb"
>•
{{
"gbEncryptedFileStorage"
| i18n: selectableProduct.PasswordManager.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>
</ng-template>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship">
{{
(selectableProduct.isAnnual
? selectableProduct.PasswordManager.basePrice / 12
: selectableProduct.PasswordManager.basePrice
) | currency: "$"
}}
/{{ "month" | i18n }},
{{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
<ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
{{ ("additionalUsers" | i18n).toLowerCase() }}
{{ {{
(selectableProduct.isAnnual (selectableProduct.isAnnual
? selectableProduct.PasswordManager.seatPrice / 12 ? selectableProduct.PasswordManager.basePrice / 12
: selectableProduct.PasswordManager.seatPrice : selectableProduct.PasswordManager.basePrice
) | currency: "$" ) | currency: "$"
}} }}
/{{ "month" | i18n }} /{{ "month" | i18n }},
</ng-container> {{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
</ng-container> <ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
</span> {{ ("additionalUsers" | i18n).toLowerCase() }}
<span {{
*ngIf=" (selectableProduct.isAnnual
!selectableProduct.PasswordManager.basePrice &&
selectableProduct.PasswordManager.hasAdditionalSeatsOption
"
>
{{
"costPerUser"
| i18n
: ((selectableProduct.isAnnual
? selectableProduct.PasswordManager.seatPrice / 12 ? selectableProduct.PasswordManager.seatPrice / 12
: selectableProduct.PasswordManager.seatPrice : selectableProduct.PasswordManager.seatPrice
) ) | currency: "$"
| currency: "$") }}
}} /{{ "month" | i18n }}
/{{ "month" | i18n }} </ng-container>
</span> </ng-container>
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span> </span>
</label> <span
</div> *ngIf="
<div *ngIf="formGroup.value.product !== productTypes.Free"> !selectableProduct.PasswordManager.basePrice &&
<ng-container selectableProduct.PasswordManager.hasAdditionalSeatsOption
"
>
{{
"costPerUser"
| i18n
: ((selectableProduct.isAnnual
? selectableProduct.PasswordManager.seatPrice / 12
: selectableProduct.PasswordManager.seatPrice
)
| currency: "$")
}}
/{{ "month" | i18n }}
</span>
<span *ngIf="selectableProduct.product == productTypes.Free">{{
"freeForever" | i18n
}}</span>
</bit-radio-group>
</div>
</bit-section>
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
<bit-section
*ngIf=" *ngIf="
selectedPlan.PasswordManager.hasAdditionalSeatsOption && selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
!selectedPlan.PasswordManager.baseSeats !selectedPlan.PasswordManager.baseSeats
" "
> >
<h2 class="mt-5">{{ "users" | i18n }}</h2> <h2 bitTypography="h2">{{ "users" | i18n }}</h2>
<div class="row"> <div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div class="col-6"> <bit-form-field class="tw-col-span-6">
<label for="additionalSeats">{{ "userSeats" | i18n }}</label> <bit-label>{{ "userSeats" | i18n }}</bit-label>
<input <input
id="additionalSeats" bitInput
class="form-control"
type="number" type="number"
name="additionalSeats"
formControlName="additionalSeats" formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}" placeholder="{{ 'userSeatsDesc' | i18n }}"
required required
/> />
<small class="text-muted form-text">{{ "userSeatsHowManyDesc" | i18n }}</small> <bit-hint class="tw-text-sm">{{ "userSeatsHowManyDesc" | i18n }}</bit-hint>
</div> </bit-form-field>
</div> </div>
</ng-container> </bit-section>
<h2 class="mt-5">{{ "addons" | i18n }}</h2> <bit-section>
<div <h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
class="row" <div
*ngIf=" class="tw-grid tw-grid-cols-12 tw-gap-4"
selectedPlan.PasswordManager.hasAdditionalSeatsOption && *ngIf="
selectedPlan.PasswordManager.baseSeats selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
" selectedPlan.PasswordManager.baseSeats
> "
<div class="form-group col-6"> >
<label for="additionalSeats">{{ "additionalUserSeats" | i18n }}</label> <bit-form-field class="tw-col-span-6">
<input <bit-label>{{ "additionalUserSeats" | i18n }}</bit-label>
id="additionalSeats"
class="form-control"
type="number"
name="additionalSeats"
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
"userSeatsAdditionalDesc"
| i18n
: selectedPlan.PasswordManager.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.PasswordManager.hasPremiumAccessOption">
<div class="form-check">
<input <input
id="premiumAccess" bitInput
class="form-check-input" type="number"
type="checkbox" formControlName="additionalSeats"
name="premiumAccessAddon" placeholder="{{ 'userSeatsDesc' | i18n }}"
formControlName="premiumAccessAddon"
/> />
<label for="premiumAccess" class="form-check-label bold">{{ <bit-hint class="tx-text-sm"
"premiumAccess" | i18n >{{
}}</label> "userSeatsAdditionalDesc"
</div> | i18n
<small class="text-muted form-text">{{ : selectedPlan.PasswordManager.baseSeats
"premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n) : (seatPriceMonthly(selectedPlan) | currency: "$")
}}</small> }}
</bit-hint>
</bit-form-field>
</div> </div>
</div> <div class="tw-grid tw-grid-cols-12 tw-gap-4">
<h2 class="spaced-header">{{ "summary" | i18n }}</h2> <bit-form-field class="tw-col-span-6">
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans"> <bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
<input <input
class="form-check-input" bitInput
type="radio" type="number"
name="plan" formControlName="additionalStorage"
id="interval{{ selectablePlan.type }}" step="1"
[value]="selectablePlan.type" placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
formControlName="plan" />
/> <bit-hint class="tw-text-sm">{{
<label class="form-check-label" for="interval{{ selectablePlan.type }}"> "additionalStorageIntervalDesc"
<ng-container *ngIf="selectablePlan.isAnnual"> | i18n
{{ "annually" | i18n }} : "1 GB"
<small *ngIf="selectablePlan.PasswordManager.basePrice"> : (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
{{ "basePrice" | i18n }}: : ("month" | i18n)
{{ }}</bit-hint>
(selectablePlan.isAnnual </bit-form-field>
? selectablePlan.PasswordManager.basePrice / 12 </div>
: selectablePlan.PasswordManager.basePrice </bit-section>
) | currency: "$" <bit-section>
}} <div
&times; 12 class="tw-grid tw-grid-cols-12 tw-gap-4"
{{ "monthAbbr" | i18n }} *ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption"
= >
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship"> <bit-form-control class="tw-col-span-6">
<span style="text-decoration: line-through">{{ <bit-label>{{ "premiumAccess" | i18n }}</bit-label>
selectablePlan.PasswordManager.basePrice | currency: "$" <input type="checkbox" bitCheckbox formControlName="premiumAccessAddon" />
}}</span> <bit-hint class="tw-text-sm">{{
{{ "freeWithSponsorship" | i18n }} "premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n)
</ng-container> }}</bit-hint>
<ng-template #notAcceptingSponsorship> </bit-form-control>
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }} </div>
</bit-section>
<bit-section *ngFor="let selectablePlan of selectablePlans">
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
<bit-radio-group formControlName="plan">
<bit-radio-button
type="radio"
id="interval{{ selectablePlan.type }}"
[value]="selectablePlan.type"
>
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
<bit-hint *ngIf="selectablePlan.isAnnual">
<p
class="tw-mb-0"
bitTypography="body2"
*ngIf="selectablePlan.PasswordManager.basePrice"
>
{{ "basePrice" | i18n }}:
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.basePrice / 12
: selectablePlan.PasswordManager.basePrice
) | currency: "$"
}}
&times; 12
{{ "monthAbbr" | i18n }}
=
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
<span class="tw-line-through">{{
selectablePlan.PasswordManager.basePrice | currency: "$"
}}</span>
{{ "freeWithSponsorship" | i18n }}
</ng-container>
<ng-template #notAcceptingSponsorship>
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
/{{ "year" | i18n }}
</ng-template>
</p>
<p
class="tw-mb-0"
bitTypography="body2"
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
>
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
>{{ "additionalUsers" | i18n }}:</span
>
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12
: selectablePlan.PasswordManager.seatPrice
) | currency: "$"
}}
&times; 12 {{ "monthAbbr" | i18n }} =
{{
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
| currency: "$"
}}
/{{ "year" | i18n }} /{{ "year" | i18n }}
</ng-template> </p>
</small> <p
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"> class="tw-mb-0"
<span *ngIf="selectablePlan.PasswordManager.baseSeats" bitTypography="body2"
>{{ "additionalUsers" | i18n }}:</span *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
> >
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span> {{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalSeats"].value || 0 }} &times; {{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ {{
(selectablePlan.isAnnual (selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12 ? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
: selectablePlan.PasswordManager.seatPrice : selectablePlan.PasswordManager.additionalStoragePricePerGb
) | currency: "$" ) | currency: "$"
}} }}
&times; 12 {{ "monthAbbr" | i18n }} = &times; 12 {{ "monthAbbr" | i18n }} =
{{ {{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats) </p>
| currency: "$" </bit-hint>
}} <bit-hint *ngIf="!selectablePlan.isAnnual">
/{{ "year" | i18n }} <p
</small> class="tw-mb-0"
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"> bitTypography="body2"
{{ "additionalStorageGb" | i18n }}: *ngIf="selectablePlan.PasswordManager.basePrice"
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
: selectablePlan.PasswordManager.additionalStoragePricePerGb
) | currency: "$"
}}
&times; 12 {{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
</small>
</ng-container>
<ng-container *ngIf="!selectablePlan.isAnnual">
{{ "monthly" | i18n }}
<small *ngIf="selectablePlan.PasswordManager.basePrice">
{{ "basePrice" | i18n }}:
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
{{ "monthAbbr" | i18n }}
=
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
/{{ "month" | i18n }}
</small>
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
>{{ "additionalUsers" | i18n }}:</span
> >
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span> {{ "basePrice" | i18n }}:
{{ formGroup.controls["additionalSeats"].value || 0 }} &times; {{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }} {{ "monthAbbr" | i18n }}
{{ "monthAbbr" | i18n }} = =
{{ {{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats) /{{ "month" | i18n }}
| currency: "$" </p>
}} <p
/{{ "month" | i18n }} class="tw-mb-0"
</small> bitTypography="body2"
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"> *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
{{ "additionalStorageGb" | i18n }}: >
{{ formGroup.controls["additionalStorage"].value || 0 }} &times; <span *ngIf="selectablePlan.PasswordManager.baseSeats"
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }} >{{ "additionalUsers" | i18n }}:</span
{{ "monthAbbr" | i18n }} = >
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }} <span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
</small> {{ formGroup.controls["additionalSeats"].value || 0 }} &times;
</ng-container> {{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
</label> {{ "monthAbbr" | i18n }} =
</div> {{
</div> passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
| currency: "$"
}}
/{{ "month" | i18n }}
</p>
<p
class="tw-mb-0"
bitTypography="body2"
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
>
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
</p>
</bit-hint>
</bit-radio-button>
</bit-radio-group>
</bit-section>
</bit-section>
<!-- Secrets Manager --> <!-- Secrets Manager -->
<div class="tw-my-10"> <bit-section>
<sm-subscribe <sm-subscribe
*ngIf="planOffersSecretsManager && !hasProvider" *ngIf="planOffersSecretsManager && !hasProvider"
[formGroup]="formGroup.controls.secretsManager" [formGroup]="formGroup.controls.secretsManager"
[selectedPlan]="selectedSecretsManagerPlan" [selectedPlan]="selectedSecretsManagerPlan"
[upgradeOrganization]="!createOrganization" [upgradeOrganization]="!createOrganization"
></sm-subscribe> ></sm-subscribe>
</div> </bit-section>
<!-- Payment info --> <!-- Payment info -->
<div *ngIf="formGroup.value.product !== productTypes.Free"> <bit-section *ngIf="formGroup.value.product !== productTypes.Free">
<h2 class="mb-4"> <h2 bitTypography="h2">
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }} {{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
</h2> </h2>
<small class="text-muted font-italic mb-3 d-block"> <p class="tw-text-muted tw-italic tw-mb-3 tw-block" bitTypography="body2">
{{ paymentDesc }} {{ paymentDesc }}
</small> </p>
<app-payment <app-payment
*ngIf="createOrganization || upgradeRequiresPaymentMethod" *ngIf="createOrganization || upgradeRequiresPaymentMethod"
[hideCredit]="true" [hideCredit]="true"
></app-payment> ></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info> <app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
<div id="price" class="my-4"> <div id="price" class="tw-my-4">
<div class="text-muted text-sm"> <div class="tw-text-muted tw-text-base">
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }} {{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
<br /> <br />
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled"> <span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
@ -405,8 +433,8 @@
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }} {{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
</ng-container> </ng-container>
</div> </div>
<hr class="my-1 col-3 ml-0" /> <hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />
<p class="text-lg"> <p class="tw-text-lg">
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ <strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
selectedPlanInterval | i18n selectedPlanInterval | i18n
}} }}
@ -415,22 +443,29 @@
<ng-container *ngIf="!createOrganization"> <ng-container *ngIf="!createOrganization">
<app-payment [showMethods]="false"></app-payment> <app-payment [showMethods]="false"></app-payment>
</ng-container> </ng-container>
</div> </bit-section>
<div *ngIf="singleOrgPolicyBlock" class="mt-4"> <bit-section *ngIf="singleOrgPolicyBlock">
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout> <app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
</div> </bit-section>
<div class="mt-4"> <bit-section>
<button <button
type="submit" type="submit"
buttonType="primary" buttonType="primary"
bitButton bitButton
[loading]="form.loading" bitFormButton
[disabled]="!formGroup.valid" [disabled]="!formGroup.valid"
> >
{{ "submit" | i18n }} {{ "submit" | i18n }}
</button> </button>
<button type="button" buttonType="secondary" bitButton (click)="cancel()" *ngIf="showCancel"> <button
type="button"
buttonType="secondary"
bitButton
bitFormButton
(click)="cancel()"
*ngIf="showCancel"
>
{{ "cancel" | i18n }} {{ "cancel" | i18n }}
</button> </button>
</div> </bit-section>
</form> </form>

View File

@ -70,6 +70,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
@Input() showCancel = false; @Input() showCancel = false;
@Input() acceptingSponsorship = false; @Input() acceptingSponsorship = false;
@Input() currentPlan: PlanResponse; @Input() currentPlan: PlanResponse;
selectedFile: File;
@Input() @Input()
get product(): ProductType { get product(): ProductType {
@ -109,6 +110,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder); secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
selfHostedForm = this.formBuilder.group({
file: [null, [Validators.required]],
});
formGroup = this.formBuilder.group({ formGroup = this.formBuilder.group({
name: [""], name: [""],
billingEmail: ["", [Validators.email]], billingEmail: ["", [Validators.email]],
@ -527,72 +532,71 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.onCanceled.emit(); this.onCanceled.emit();
} }
async submit() { setSelectedFile(event: Event) {
const fileInputEl = <HTMLInputElement>event.target;
this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
}
submit = async () => {
if (this.singleOrgPolicyBlock) { if (this.singleOrgPolicyBlock) {
return; return;
} }
const doSubmit = async (): Promise<string> => {
let orgId: string = null;
if (this.createOrganization) {
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
const collection = await this.cryptoService.encrypt(
this.i18nService.t("defaultCollection"),
orgKey[1],
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
try { if (this.selfHosted) {
const doSubmit = async (): Promise<string> => { orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
let orgId: string = null;
if (this.createOrganization) {
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
const collection = await this.cryptoService.encrypt(
this.i18nService.t("defaultCollection"),
orgKey[1],
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} else {
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
}
this.platformUtilsService.showToast(
"success",
this.i18nService.t("organizationCreated"),
this.i18nService.t("organizationReadyToGo"),
);
} else { } else {
orgId = await this.updateOrganization(orgId); orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationUpgraded"),
);
} }
await this.apiService.refreshIdentityToken(); this.platformUtilsService.showToast(
await this.syncService.fullSync(true); "success",
this.i18nService.t("organizationCreated"),
this.i18nService.t("organizationReadyToGo"),
);
} else {
orgId = await this.updateOrganization(orgId);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationUpgraded"),
);
}
if (!this.acceptingSponsorship && !this.isInTrialFlow) { await this.apiService.refreshIdentityToken();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.syncService.fullSync(true);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/organizations/" + orgId]);
}
if (this.isInTrialFlow) { if (!this.acceptingSponsorship && !this.isInTrialFlow) {
this.onTrialBillingSuccess.emit({ // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
orgId: orgId, // eslint-disable-next-line @typescript-eslint/no-floating-promises
subLabelText: this.billingSubLabelText(), this.router.navigate(["/organizations/" + orgId]);
}); }
}
return orgId; if (this.isInTrialFlow) {
}; this.onTrialBillingSuccess.emit({
orgId: orgId,
subLabelText: this.billingSubLabelText(),
});
}
this.formPromise = doSubmit(); return orgId;
const organizationId = await this.formPromise; };
this.onSuccess.emit({ organizationId: organizationId });
// TODO: No one actually listening to this message? this.formPromise = doSubmit();
this.messagingService.send("organizationCreated", { organizationId }); const organizationId = await this.formPromise;
} catch (e) { this.onSuccess.emit({ organizationId: organizationId });
this.logService.error(e); this.messagingService.send("organizationCreated", organizationId);
} };
}
private async updateOrganization(orgId: string) { private async updateOrganization(orgId: string) {
const request = new OrganizationUpgradeRequest(); const request = new OrganizationUpgradeRequest();
@ -693,14 +697,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
} }
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) { private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
const fileEl = document.getElementById("file") as HTMLInputElement; if (!this.selectedFile) {
const files = fileEl.files;
if (files == null || files.length === 0) {
throw new Error(this.i18nService.t("selectFile")); throw new Error(this.i18nService.t("selectFile"));
} }
const fd = new FormData(); const fd = new FormData();
fd.append("license", files[0]); fd.append("license", this.selectedFile);
fd.append("key", key); fd.append("key", key);
fd.append("collectionName", collectionCt); fd.append("collectionName", collectionCt);
const response = await this.organizationApiService.createLicense(fd); const response = await this.organizationApiService.createLicense(fd);