mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-14 20:01:31 +01:00
[AC-1706] Show Discounted Prices (#6668)
* Removed subscription copy from org and individual * Discount all prices in subscription components
This commit is contained in:
parent
8067b26dc6
commit
95d4d281cb
@ -90,17 +90,6 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}</td>
|
<td>{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="discount != null && discount.active">
|
|
||||||
<td colspan="2">
|
|
||||||
<small>
|
|
||||||
{{ "customBillingStart" | i18n }}
|
|
||||||
<a routerLink="/settings/subscription/billing-history">
|
|
||||||
{{ "billingHistory" | i18n }}
|
|
||||||
</a>
|
|
||||||
{{ "customBillingEnd" | i18n }}
|
|
||||||
</small>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -205,10 +205,6 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
return this.sub != null ? this.sub.upcomingInvoice : null;
|
return this.sub != null ? this.sub.upcomingInvoice : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get discount() {
|
|
||||||
return this.sub != null ? this.sub.discount : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get storagePercentage() {
|
get storagePercentage() {
|
||||||
return this.sub != null && this.sub.maxStorageGb
|
return this.sub != null && this.sub.maxStorageGb
|
||||||
? +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2)
|
? +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2)
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
<bit-table>
|
<bit-table>
|
||||||
<ng-template body>
|
<ng-template body>
|
||||||
<ng-container *ngIf="subscription">
|
<ng-container *ngIf="subscription">
|
||||||
<tr bitRow *ngFor="let i of lineItems">
|
<tr bitRow *ngFor="let i of subscriptionLineItems">
|
||||||
<td bitCell [ngClass]="{ 'tw-pl-20': i.addonSubscriptionItem }">
|
<td bitCell [ngClass]="{ 'tw-pl-20': i.addonSubscriptionItem }">
|
||||||
<span *ngIf="!i.addonSubscriptionItem">{{ i.productName }} -</span>
|
<span *ngIf="!i.addonSubscriptionItem">{{ i.productName }} -</span>
|
||||||
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
||||||
@ -79,17 +79,6 @@
|
|||||||
{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}
|
{{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr bitRow *ngIf="discount && discount.active">
|
|
||||||
<td bitCell colspan="2">
|
|
||||||
<small>
|
|
||||||
{{ "customBillingStart" | i18n }}
|
|
||||||
<a routerLink="/settings/subscription/billing-history">
|
|
||||||
{{ "billingHistory" | i18n }}
|
|
||||||
</a>
|
|
||||||
{{ "customBillingEnd" | i18n }}
|
|
||||||
</small>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="userOrg.isFreeOrg">
|
<ng-container *ngIf="userOrg.isFreeOrg">
|
||||||
<tr bitRow *ngIf="userOrg.usePasswordManager">
|
<tr bitRow *ngIf="userOrg.usePasswordManager">
|
||||||
@ -150,6 +139,7 @@
|
|||||||
<sm-subscribe-standalone
|
<sm-subscribe-standalone
|
||||||
[plan]="sub.plan"
|
[plan]="sub.plan"
|
||||||
[organization]="userOrg"
|
[organization]="userOrg"
|
||||||
|
[customerDiscount]="customerDiscount"
|
||||||
(onSubscribe)="subscriptionAdjusted()"
|
(onSubscribe)="subscriptionAdjusted()"
|
||||||
></sm-subscribe-standalone>
|
></sm-subscribe-standalone>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,12 +134,24 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
return this.sub != null ? this.sub.subscription : null;
|
return this.sub != null ? this.sub.subscription : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get subscriptionLineItems() {
|
||||||
|
return this.lineItems.map((lineItem: BillingSubscriptionItemResponse) => ({
|
||||||
|
name: lineItem.name,
|
||||||
|
amount: this.discountPrice(lineItem.amount),
|
||||||
|
quantity: lineItem.quantity,
|
||||||
|
interval: lineItem.interval,
|
||||||
|
sponsoredSubscriptionItem: lineItem.sponsoredSubscriptionItem,
|
||||||
|
addonSubscriptionItem: lineItem.addonSubscriptionItem,
|
||||||
|
productName: lineItem.productName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
get nextInvoice() {
|
get nextInvoice() {
|
||||||
return this.sub != null ? this.sub.upcomingInvoice : null;
|
return this.sub != null ? this.sub.upcomingInvoice : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get discount() {
|
get customerDiscount() {
|
||||||
return this.sub != null ? this.sub.discount : null;
|
return this.sub != null ? this.sub.customerDiscount : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isExpired() {
|
get isExpired() {
|
||||||
@ -168,11 +180,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
}
|
}
|
||||||
|
|
||||||
get storageGbPrice() {
|
get storageGbPrice() {
|
||||||
return this.sub.plan.PasswordManager.additionalStoragePricePerGb;
|
return this.discountPrice(this.sub.plan.PasswordManager.additionalStoragePricePerGb);
|
||||||
}
|
}
|
||||||
|
|
||||||
get seatPrice() {
|
get seatPrice() {
|
||||||
return this.sub.plan.PasswordManager.seatPrice;
|
return this.discountPrice(this.sub.plan.PasswordManager.seatPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
get seats() {
|
get seats() {
|
||||||
@ -183,12 +195,14 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
return {
|
return {
|
||||||
seatCount: this.sub.smSeats,
|
seatCount: this.sub.smSeats,
|
||||||
maxAutoscaleSeats: this.sub.maxAutoscaleSmSeats,
|
maxAutoscaleSeats: this.sub.maxAutoscaleSmSeats,
|
||||||
seatPrice: this.sub.plan.SecretsManager.seatPrice,
|
seatPrice: this.discountPrice(this.sub.plan.SecretsManager.seatPrice),
|
||||||
maxAutoscaleServiceAccounts: this.sub.maxAutoscaleSmServiceAccounts,
|
maxAutoscaleServiceAccounts: this.sub.maxAutoscaleSmServiceAccounts,
|
||||||
additionalServiceAccounts:
|
additionalServiceAccounts:
|
||||||
this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount,
|
this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount,
|
||||||
interval: this.sub.plan.isAnnual ? "year" : "month",
|
interval: this.sub.plan.isAnnual ? "year" : "month",
|
||||||
additionalServiceAccountPrice: this.sub.plan.SecretsManager.additionalPricePerServiceAccount,
|
additionalServiceAccountPrice: this.discountPrice(
|
||||||
|
this.sub.plan.SecretsManager.additionalPricePerServiceAccount
|
||||||
|
),
|
||||||
baseServiceAccountCount: this.sub.plan.SecretsManager.baseServiceAccount,
|
baseServiceAccountCount: this.sub.plan.SecretsManager.baseServiceAccount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -382,6 +396,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
discountPrice = (price: number) => {
|
||||||
|
const discount =
|
||||||
|
!!this.customerDiscount && this.customerDiscount.active
|
||||||
|
? price * (this.customerDiscount.percentOff / 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return price - discount;
|
||||||
|
};
|
||||||
|
|
||||||
get showChangePlanButton() {
|
get showChangePlanButton() {
|
||||||
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
||||||
}
|
}
|
||||||
|
@ -4,5 +4,6 @@
|
|||||||
[selectedPlan]="plan"
|
[selectedPlan]="plan"
|
||||||
[upgradeOrganization]="false"
|
[upgradeOrganization]="false"
|
||||||
[showSubmitButton]="true"
|
[showSubmitButton]="true"
|
||||||
|
[customerDiscount]="customerDiscount"
|
||||||
></sm-subscribe>
|
></sm-subscribe>
|
||||||
</form>
|
</form>
|
||||||
|
@ -6,6 +6,7 @@ import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-
|
|||||||
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
|
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request";
|
import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request";
|
||||||
|
import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -19,6 +20,7 @@ import { secretsManagerSubscribeFormFactory } from "../shared";
|
|||||||
export class SecretsManagerSubscribeStandaloneComponent {
|
export class SecretsManagerSubscribeStandaloneComponent {
|
||||||
@Input() plan: PlanResponse;
|
@Input() plan: PlanResponse;
|
||||||
@Input() organization: Organization;
|
@Input() organization: Organization;
|
||||||
|
@Input() customerDiscount: BillingCustomerDiscount;
|
||||||
@Output() onSubscribe = new EventEmitter<void>();
|
@Output() onSubscribe = new EventEmitter<void>();
|
||||||
|
|
||||||
formGroup = secretsManagerSubscribeFormFactory(this.formBuilder);
|
formGroup = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||||
|
@ -3,6 +3,7 @@ import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
|||||||
import { Subject, startWith, takeUntil } from "rxjs";
|
import { Subject, startWith, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
|
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
|
||||||
|
import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -36,6 +37,7 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy {
|
|||||||
@Input() upgradeOrganization: boolean;
|
@Input() upgradeOrganization: boolean;
|
||||||
@Input() showSubmitButton = false;
|
@Input() showSubmitButton = false;
|
||||||
@Input() selectedPlan: PlanResponse;
|
@Input() selectedPlan: PlanResponse;
|
||||||
|
@Input() customerDiscount: BillingCustomerDiscount;
|
||||||
|
|
||||||
logo = SecretsManagerLogo;
|
logo = SecretsManagerLogo;
|
||||||
productTypes = ProductType;
|
productTypes = ProductType;
|
||||||
@ -63,6 +65,15 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
discountPrice = (price: number) => {
|
||||||
|
const discount =
|
||||||
|
!!this.customerDiscount && this.customerDiscount.active
|
||||||
|
? price * (this.customerDiscount.percentOff / 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return price - discount;
|
||||||
|
};
|
||||||
|
|
||||||
get product() {
|
get product() {
|
||||||
return this.selectedPlan.product;
|
return this.selectedPlan.product;
|
||||||
}
|
}
|
||||||
@ -84,8 +95,8 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
get monthlyCostPerServiceAccount() {
|
get monthlyCostPerServiceAccount() {
|
||||||
return this.selectedPlan.isAnnual
|
return this.selectedPlan.isAnnual
|
||||||
? this.selectedPlan.SecretsManager.additionalPricePerServiceAccount / 12
|
? this.discountPrice(this.selectedPlan.SecretsManager.additionalPricePerServiceAccount) / 12
|
||||||
: this.selectedPlan.SecretsManager.additionalPricePerServiceAccount;
|
: this.discountPrice(this.selectedPlan.SecretsManager.additionalPricePerServiceAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxUsers() {
|
get maxUsers() {
|
||||||
@ -98,7 +109,7 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
get monthlyCostPerUser() {
|
get monthlyCostPerUser() {
|
||||||
return this.selectedPlan.isAnnual
|
return this.selectedPlan.isAnnual
|
||||||
? this.selectedPlan.SecretsManager.seatPrice / 12
|
? this.discountPrice(this.selectedPlan.SecretsManager.seatPrice) / 12
|
||||||
: this.selectedPlan.SecretsManager.seatPrice;
|
: this.discountPrice(this.selectedPlan.SecretsManager.seatPrice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7273,12 +7273,6 @@
|
|||||||
"alreadyHaveAccount": {
|
"alreadyHaveAccount": {
|
||||||
"message": "Already have an account?"
|
"message": "Already have an account?"
|
||||||
},
|
},
|
||||||
"customBillingStart": {
|
|
||||||
"message": "Custom billing is not reflected. Visit the "
|
|
||||||
},
|
|
||||||
"customBillingEnd": {
|
|
||||||
"message": " page for latest invoicing."
|
|
||||||
},
|
|
||||||
"typePasskey": {
|
"typePasskey": {
|
||||||
"message": "Passkey"
|
"message": "Passkey"
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { OrganizationResponse } from "../../../admin-console/models/response/organization.response";
|
import { OrganizationResponse } from "../../../admin-console/models/response/organization.response";
|
||||||
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BillingSubscriptionResponse,
|
BillingSubscriptionResponse,
|
||||||
BillingSubscriptionUpcomingInvoiceResponse,
|
BillingSubscriptionUpcomingInvoiceResponse,
|
||||||
BillingCustomerDiscount,
|
|
||||||
} from "./subscription.response";
|
} from "./subscription.response";
|
||||||
|
|
||||||
export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
||||||
@ -11,7 +11,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
|||||||
storageGb: number;
|
storageGb: number;
|
||||||
subscription: BillingSubscriptionResponse;
|
subscription: BillingSubscriptionResponse;
|
||||||
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
||||||
discount: BillingCustomerDiscount;
|
customerDiscount: BillingCustomerDiscount;
|
||||||
expiration: string;
|
expiration: string;
|
||||||
expirationWithoutGracePeriod: string;
|
expirationWithoutGracePeriod: string;
|
||||||
secretsManagerBeta: boolean;
|
secretsManagerBeta: boolean;
|
||||||
@ -27,10 +27,30 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
|
|||||||
upcomingInvoice == null
|
upcomingInvoice == null
|
||||||
? null
|
? null
|
||||||
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
||||||
const discount = this.getResponseProperty("Discount");
|
const customerDiscount = this.getResponseProperty("CustomerDiscount");
|
||||||
this.discount = discount == null ? null : new BillingCustomerDiscount(discount);
|
this.customerDiscount =
|
||||||
|
customerDiscount == null ? null : new BillingCustomerDiscount(customerDiscount);
|
||||||
this.expiration = this.getResponseProperty("Expiration");
|
this.expiration = this.getResponseProperty("Expiration");
|
||||||
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
|
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
|
||||||
this.secretsManagerBeta = this.getResponseProperty("SecretsManagerBeta");
|
this.secretsManagerBeta = this.getResponseProperty("SecretsManagerBeta");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BillingCustomerDiscount extends BaseResponse {
|
||||||
|
id: string;
|
||||||
|
active: boolean;
|
||||||
|
percentOff?: number;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.id = this.getResponseProperty("Id");
|
||||||
|
this.active = this.getResponseProperty("Active");
|
||||||
|
this.percentOff = this.getResponseProperty("PercentOff");
|
||||||
|
}
|
||||||
|
|
||||||
|
discountPrice = (price: number) => {
|
||||||
|
const discount = this !== null && this.active ? price * (this.percentOff / 100) : 0;
|
||||||
|
|
||||||
|
return price - discount;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -6,7 +6,6 @@ export class SubscriptionResponse extends BaseResponse {
|
|||||||
maxStorageGb: number;
|
maxStorageGb: number;
|
||||||
subscription: BillingSubscriptionResponse;
|
subscription: BillingSubscriptionResponse;
|
||||||
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
|
||||||
discount: BillingCustomerDiscount;
|
|
||||||
license: any;
|
license: any;
|
||||||
expiration: string;
|
expiration: string;
|
||||||
usingInAppPurchase: boolean;
|
usingInAppPurchase: boolean;
|
||||||
@ -21,13 +20,11 @@ export class SubscriptionResponse extends BaseResponse {
|
|||||||
this.usingInAppPurchase = this.getResponseProperty("UsingInAppPurchase");
|
this.usingInAppPurchase = this.getResponseProperty("UsingInAppPurchase");
|
||||||
const subscription = this.getResponseProperty("Subscription");
|
const subscription = this.getResponseProperty("Subscription");
|
||||||
const upcomingInvoice = this.getResponseProperty("UpcomingInvoice");
|
const upcomingInvoice = this.getResponseProperty("UpcomingInvoice");
|
||||||
const discount = this.getResponseProperty("Discount");
|
|
||||||
this.subscription = subscription == null ? null : new BillingSubscriptionResponse(subscription);
|
this.subscription = subscription == null ? null : new BillingSubscriptionResponse(subscription);
|
||||||
this.upcomingInvoice =
|
this.upcomingInvoice =
|
||||||
upcomingInvoice == null
|
upcomingInvoice == null
|
||||||
? null
|
? null
|
||||||
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
|
||||||
this.discount = discount == null ? null : new BillingCustomerDiscount(discount);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,14 +86,3 @@ export class BillingSubscriptionUpcomingInvoiceResponse extends BaseResponse {
|
|||||||
this.amount = this.getResponseProperty("Amount");
|
this.amount = this.getResponseProperty("Amount");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BillingCustomerDiscount extends BaseResponse {
|
|
||||||
id: string;
|
|
||||||
active: boolean;
|
|
||||||
|
|
||||||
constructor(response: any) {
|
|
||||||
super(response);
|
|
||||||
this.id = this.getResponseProperty("Id");
|
|
||||||
this.active = this.getResponseProperty("Active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user