diff --git a/apps/web/src/app/modules/billing/billing.component.html b/apps/web/src/app/modules/billing/billing.component.html new file mode 100644 index 00000000000..865c234c006 --- /dev/null +++ b/apps/web/src/app/modules/billing/billing.component.html @@ -0,0 +1,48 @@ +
+
+
+

{{ "billingPlanLabel" | i18n }}

+
+ +
+
+ +
+

{{ "paymentType" | i18n }}

+ + +
+ +
+ {{ "startTrial" | i18n }} + + +
+
+
diff --git a/apps/web/src/app/modules/billing/billing.component.ts b/apps/web/src/app/modules/billing/billing.component.ts new file mode 100644 index 00000000000..ed0eb7892e7 --- /dev/null +++ b/apps/web/src/app/modules/billing/billing.component.ts @@ -0,0 +1,68 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; +import { Router } from "@angular/router"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { PolicyService } from "@bitwarden/common/abstractions/policy.service"; +import { SyncService } from "@bitwarden/common/abstractions/sync.service"; + +import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component"; + +@Component({ + selector: "app-billing", + templateUrl: "./billing.component.html", +}) +export class BillingComponent extends OrganizationPlansComponent { + @Input() orgInfoForm: FormGroup; + @Output() previousStep = new EventEmitter(); + + constructor( + apiService: ApiService, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + cryptoService: CryptoService, + router: Router, + syncService: SyncService, + policyService: PolicyService, + organizationService: OrganizationService, + logService: LogService, + messagingService: MessagingService, + formBuilder: FormBuilder + ) { + super( + apiService, + i18nService, + platformUtilsService, + cryptoService, + router, + syncService, + policyService, + organizationService, + logService, + messagingService, + formBuilder + ); + } + + async ngOnInit() { + this.formGroup.patchValue({ + name: this.orgInfoForm.get("name")?.value, + billingEmail: this.orgInfoForm.get("email")?.value, + additionalSeats: 1, + plan: this.plan, + product: this.product, + }); + this.isInTrialFlow = true; + await super.ngOnInit(); + } + + stepBack() { + this.previousStep.emit(); + } +} diff --git a/apps/web/src/app/modules/billing/billing.module.ts b/apps/web/src/app/modules/billing/billing.module.ts new file mode 100644 index 00000000000..dc8b9f1f37c --- /dev/null +++ b/apps/web/src/app/modules/billing/billing.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../shared.module"; + +import { BillingComponent } from "./billing.component"; + +@NgModule({ + imports: [SharedModule], + declarations: [BillingComponent], + exports: [BillingComponent], +}) +export class BillingModule {} diff --git a/apps/web/src/app/modules/loose-components.module.ts b/apps/web/src/app/modules/loose-components.module.ts index a84ac4fb9a7..37b6d8172ff 100644 --- a/apps/web/src/app/modules/loose-components.module.ts +++ b/apps/web/src/app/modules/loose-components.module.ts @@ -114,7 +114,6 @@ import { EmergencyAccessComponent } from "../settings/emergency-access.component import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component"; import { OrganizationPlansComponent } from "../settings/organization-plans.component"; import { PaymentMethodComponent } from "../settings/payment-method.component"; -import { PaymentComponent } from "../settings/payment.component"; import { PreferencesComponent } from "../settings/preferences.component"; import { PremiumComponent } from "../settings/premium.component"; import { ProfileComponent } from "../settings/profile.component"; @@ -125,7 +124,6 @@ import { SettingsComponent } from "../settings/settings.component"; import { SponsoredFamiliesComponent } from "../settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../settings/sponsoring-org-row.component"; import { SubscriptionComponent } from "../settings/subscription.component"; -import { TaxInfoComponent } from "../settings/tax-info.component"; import { TwoFactorAuthenticatorComponent } from "../settings/two-factor-authenticator.component"; import { TwoFactorDuoComponent } from "../settings/two-factor-duo.component"; import { TwoFactorEmailComponent } from "../settings/two-factor-email.component"; @@ -271,7 +269,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga PasswordGeneratorHistoryComponent, PasswordGeneratorPolicyComponent, PasswordRepromptComponent, - PaymentComponent, PaymentMethodComponent, PersonalOwnershipPolicyComponent, PreferencesComponent, @@ -304,7 +301,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga SponsoringOrgRowComponent, SsoComponent, SubscriptionComponent, - TaxInfoComponent, ToolsComponent, TwoFactorAuthenticationPolicyComponent, TwoFactorAuthenticatorComponent, @@ -425,7 +421,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga PasswordGeneratorHistoryComponent, PasswordGeneratorPolicyComponent, PasswordRepromptComponent, - PaymentComponent, PaymentMethodComponent, PersonalOwnershipPolicyComponent, PreferencesComponent, @@ -458,7 +453,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga SponsoringOrgRowComponent, SsoComponent, SubscriptionComponent, - TaxInfoComponent, ToolsComponent, TwoFactorAuthenticationPolicyComponent, TwoFactorAuthenticatorComponent, diff --git a/apps/web/src/app/modules/shared.module.ts b/apps/web/src/app/modules/shared.module.ts index 1bfbe632918..79f967f23a7 100644 --- a/apps/web/src/app/modules/shared.module.ts +++ b/apps/web/src/app/modules/shared.module.ts @@ -67,6 +67,8 @@ import { } from "@bitwarden/components"; import { PasswordStrengthComponent } from "../components/password-strength.component"; +import { PaymentComponent } from "../settings/payment.component"; +import { TaxInfoComponent } from "../settings/tax-info.component"; registerLocaleData(localeAf, "af"); registerLocaleData(localeAz, "az"); @@ -120,7 +122,7 @@ registerLocaleData(localeZhCn, "zh-CN"); registerLocaleData(localeZhTw, "zh-TW"); @NgModule({ - declarations: [PasswordStrengthComponent], + declarations: [PasswordStrengthComponent, PaymentComponent, TaxInfoComponent], imports: [ CommonModule, DragDropModule, @@ -155,8 +157,10 @@ registerLocaleData(localeZhTw, "zh-TW"); ButtonModule, MenuModule, FormFieldModule, - PasswordStrengthComponent, SubmitButtonModule, + PasswordStrengthComponent, + PaymentComponent, + TaxInfoComponent, ], providers: [DatePipe], bootstrap: [], diff --git a/apps/web/src/app/modules/trial-initiation/trial-initiation.component.html b/apps/web/src/app/modules/trial-initiation/trial-initiation.component.html index 25037ce8550..2c24377e7d0 100644 --- a/apps/web/src/app/modules/trial-initiation/trial-initiation.component.html +++ b/apps/web/src/app/modules/trial-initiation/trial-initiation.component.html @@ -57,11 +57,15 @@ Next - - -

This is content of "Step 3"

- - + + diff --git a/apps/web/src/app/modules/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/modules/trial-initiation/trial-initiation.component.ts index 188cf857cbd..88bdd3576e2 100644 --- a/apps/web/src/app/modules/trial-initiation/trial-initiation.component.ts +++ b/apps/web/src/app/modules/trial-initiation/trial-initiation.component.ts @@ -10,6 +10,8 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; +import { PlanType } from "@bitwarden/common/enums/planType"; +import { ProductType } from "@bitwarden/common/enums/productType"; import { PolicyData } from "@bitwarden/common/models/data/policyData"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions"; import { Policy } from "@bitwarden/common/models/domain/policy"; @@ -24,6 +26,10 @@ export class TrialInitiationComponent implements OnInit { email = ""; org = "teams"; orgInfoSubLabel = ""; + orgId = ""; + billingSubLabel = ""; + plan: PlanType; + product: ProductType; accountCreateOnly = true; policies: Policy[]; enforcedPolicyOptions: MasterPasswordPolicyOptions; @@ -31,11 +37,7 @@ export class TrialInitiationComponent implements OnInit { orgInfoFormGroup = this.formBuilder.group({ name: ["", [Validators.required]], - additionalStorage: [0, [Validators.min(0), Validators.max(99)]], - additionalSeats: [0, [Validators.min(0), Validators.max(100000)]], - businessName: [""], - plan: [], - product: [], + email: [""], }); constructor( @@ -54,9 +56,23 @@ export class TrialInitiationComponent implements OnInit { if (qParams.email != null && qParams.email.indexOf("@") > -1) { this.email = qParams.email; } - if (qParams.org) { - this.org = qParams.org; - this.accountCreateOnly = false; + + if (!qParams.org) { + return; + } + + this.org = qParams.org; + this.accountCreateOnly = false; + + if (qParams.org === "families") { + this.plan = PlanType.FamiliesAnnually; + this.product = ProductType.Families; + } else if (qParams.org === "teams") { + this.plan = PlanType.TeamsAnnually; + this.product = ProductType.Teams; + } else if (qParams.org === "enterprise") { + this.plan = PlanType.EnterpriseAnnually; + this.product = ProductType.Enterprise; } }); @@ -93,10 +109,26 @@ export class TrialInitiationComponent implements OnInit { } else if (event.previouslySelectedIndex === 1) { this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value; } + + //set billing sub label + if (event.selectedIndex === 2) { + this.billingSubLabel = this.i18nService.t("billingTrialSubLabel"); + } } createdAccount(email: string) { this.email = email; + this.orgInfoFormGroup.get("email")?.setValue(email); this.verticalStepper.next(); } + + billingSuccess(event: any) { + this.orgId = event?.orgId; + this.billingSubLabel = event?.subLabelText; + this.verticalStepper.next(); + } + + previousStep() { + this.verticalStepper.previous(); + } } diff --git a/apps/web/src/app/modules/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/modules/trial-initiation/trial-initiation.module.ts index 1df93915802..f9e9b3c9113 100644 --- a/apps/web/src/app/modules/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/modules/trial-initiation/trial-initiation.module.ts @@ -9,6 +9,7 @@ import { RegisterFormModule } from "../register-form/register-form.module"; import { SharedModule } from "../shared.module"; import { VerticalStepperModule } from "../vertical-stepper/vertical-stepper.module"; +import { BillingModule } from "./../billing/billing.module"; import { EnterpriseContentComponent } from "./enterprise-content.component"; import { FamiliesContentComponent } from "./families-content.component"; import { TeamsContentComponent } from "./teams-content.component"; @@ -22,6 +23,7 @@ import { TrialInitiationComponent } from "./trial-initiation.component"; FormFieldModule, RegisterFormModule, OrganizationCreateModule, + BillingModule, ], declarations: [ TrialInitiationComponent, diff --git a/apps/web/src/app/settings/organization-plans.component.ts b/apps/web/src/app/settings/organization-plans.component.ts index a541f37ec20..25e35ade7e7 100644 --- a/apps/web/src/app/settings/organization-plans.component.ts +++ b/apps/web/src/app/settings/organization-plans.component.ts @@ -43,12 +43,14 @@ export class OrganizationPlansComponent implements OnInit { @Input() providerId: string; @Output() onSuccess = new EventEmitter(); @Output() onCanceled = new EventEmitter(); + @Output() onTrialBillingSuccess = new EventEmitter(); loading = true; selfHosted = false; productTypes = ProductType; formPromise: Promise; singleOrgPolicyBlock = false; + isInTrialFlow = false; discount = 0; formGroup = this.formBuilder.group({ @@ -149,7 +151,7 @@ export class OrganizationPlansComponent implements OnInit { } get selectablePlans() { - return this.plans.filter( + return this.plans?.filter( (plan) => !plan.legacyYear && !plan.disabled && plan.product === this.formGroup.controls.product.value ); @@ -321,10 +323,18 @@ export class OrganizationPlansComponent implements OnInit { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); - if (!this.acceptingSponsorship) { + + if (!this.acceptingSponsorship && !this.isInTrialFlow) { this.router.navigate(["/organizations/" + orgId]); } + if (this.isInTrialFlow) { + this.onTrialBillingSuccess.emit({ + orgId: orgId, + subLabelText: this.billingSubLabelText(), + }); + } + return orgId; }; @@ -448,4 +458,18 @@ export class OrganizationPlansComponent implements OnInit { return orgId; } + + private billingSubLabelText(): string { + const selectedPlan = this.selectedPlan; + const price = selectedPlan.basePrice === 0 ? selectedPlan.seatPrice : selectedPlan.basePrice; + let text = ""; + + if (selectedPlan.isAnnual) { + text += `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`; + } else { + text += `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`; + } + + return text; + } } diff --git a/apps/web/src/app/settings/payment.component.html b/apps/web/src/app/settings/payment.component.html index 66135e0b9af..be9057d0107 100644 --- a/apps/web/src/app/settings/payment.component.html +++ b/apps/web/src/app/settings/payment.component.html @@ -58,11 +58,11 @@
-
+
-
+
Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay
-
+
-
+
-
+
> { - const r = await this.send("GET", "/plans/", null, true, true); + const r = await this.send("GET", "/plans/", null, false, true); return new ListResponse(r, PlanResponse); }