diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 63464b4de8..baa4e0066c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -423,10 +423,13 @@ "invalidEmail": { "message": "Invalid email address." }, - "masterPassRequired": { + "masterPasswordRequired": { "message": "Master password is required." }, - "masterPassLength": { + "confirmMasterPasswordRequired": { + "message": "Master password retype is required." + }, + "masterPasswordMinLength": { "message": "Master password must be at least 8 characters long." }, "masterPassDoesntMatch": { @@ -1480,7 +1483,7 @@ "acceptPolicies": { "message": "By checking this box you agree to the following:" }, - "acceptPoliciesError": { + "acceptPoliciesRequired": { "message": "Terms of Service and Privacy Policy have not been acknowledged." }, "termsOfService": { diff --git a/apps/browser/src/popup/accounts/register.component.html b/apps/browser/src/popup/accounts/register.component.html index fc9c4e7990..4c8ec89aac 100644 --- a/apps/browser/src/popup/accounts/register.component.html +++ b/apps/browser/src/popup/accounts/register.component.html @@ -1,4 +1,4 @@ -
+
{{ "cancel" | i18n }} @@ -18,16 +18,7 @@
- +
@@ -44,11 +35,8 @@ @@ -60,7 +48,7 @@ appStopClick appBlurClick appA11yTitle="{{ 'toggleVisibility' | i18n }}" - (click)="togglePassword(false)" + (click)="togglePassword()" [attr.aria-pressed]="showPassword" >
@@ -110,7 +96,7 @@ appStopClick appBlurClick appA11yTitle="{{ 'toggleVisibility' | i18n }}" - (click)="togglePassword(true)" + (click)="togglePassword()" [attr.aria-pressed]="showPassword" >
- +
- + - + diff --git a/apps/web/src/app/accounts/register.component.ts b/apps/web/src/app/accounts/register.component.ts index 3ae38f960e..c4d60c4736 100644 --- a/apps/web/src/app/accounts/register.component.ts +++ b/apps/web/src/app/accounts/register.component.ts @@ -1,4 +1,5 @@ import { Component } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; @@ -7,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; +import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; @@ -25,6 +27,7 @@ import { RouterService } from "../services/router.service"; templateUrl: "register.component.html", }) export class RegisterComponent extends BaseRegisterComponent { + email = ""; showCreateOrgMessage = false; layout = ""; enforcedPolicyOptions: MasterPasswordPolicyOptions; @@ -32,6 +35,8 @@ export class RegisterComponent extends BaseRegisterComponent { private policies: Policy[]; constructor( + formValidationErrorService: FormValidationErrorsService, + formBuilder: FormBuilder, authService: AuthService, router: Router, i18nService: I18nService, @@ -47,6 +52,8 @@ export class RegisterComponent extends BaseRegisterComponent { private routerService: RouterService ) { super( + formValidationErrorService, + formBuilder, authService, router, i18nService, @@ -126,24 +133,4 @@ export class RegisterComponent extends BaseRegisterComponent { await super.ngOnInit(); } - - async submit() { - if ( - this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - this.masterPasswordScore, - this.masterPassword, - this.enforcedPolicyOptions - ) - ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordPolicyRequirementsNotMet") - ); - return; - } - - await super.submit(); - } } diff --git a/apps/web/src/app/modules/loose-components.module.ts b/apps/web/src/app/modules/loose-components.module.ts index 7fdce78d5d..37a87cee36 100644 --- a/apps/web/src/app/modules/loose-components.module.ts +++ b/apps/web/src/app/modules/loose-components.module.ts @@ -22,7 +22,6 @@ import { VerifyRecoverDeleteComponent } from "../accounts/verify-recover-delete. import { NestedCheckboxComponent } from "../components/nested-checkbox.component"; import { OrganizationSwitcherComponent } from "../components/organization-switcher.component"; import { PasswordRepromptComponent } from "../components/password-reprompt.component"; -import { PasswordStrengthComponent } from "../components/password-strength.component"; import { PremiumBadgeComponent } from "../components/premium-badge.component"; import { FooterComponent } from "../layouts/footer.component"; import { FrontendLayoutComponent } from "../layouts/frontend-layout.component"; @@ -158,6 +157,7 @@ import { FolderAddEditComponent } from "../vault/folder-add-edit.component"; import { ShareComponent } from "../vault/share.component"; import { PipesModule } from "./pipes/pipes.module"; +import { RegisterFormModule } from "./register-form/register-form.module"; import { SharedModule } from "./shared.module"; import { VaultFilterModule } from "./vault-filter/vault-filter.module"; import { OrganizationBadgeModule } from "./vault/modules/organization-badge/organization-badge.module"; @@ -165,7 +165,13 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga // Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left. // If you are building new functionality, please create or extend a feature module instead. @NgModule({ - imports: [SharedModule, VaultFilterModule, OrganizationBadgeModule, PipesModule], + imports: [ + SharedModule, + VaultFilterModule, + OrganizationBadgeModule, + PipesModule, + RegisterFormModule, + ], declarations: [ PremiumBadgeComponent, AcceptEmergencyComponent, @@ -263,7 +269,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga PasswordGeneratorHistoryComponent, PasswordGeneratorPolicyComponent, PasswordRepromptComponent, - PasswordStrengthComponent, PaymentComponent, PaymentMethodComponent, PersonalOwnershipPolicyComponent, @@ -418,7 +423,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga PasswordGeneratorHistoryComponent, PasswordGeneratorPolicyComponent, PasswordRepromptComponent, - PasswordStrengthComponent, PaymentComponent, PaymentMethodComponent, PersonalOwnershipPolicyComponent, diff --git a/apps/web/src/app/modules/register-form/register-form.component.html b/apps/web/src/app/modules/register-form/register-form.component.html new file mode 100644 index 0000000000..b4f2877ef1 --- /dev/null +++ b/apps/web/src/app/modules/register-form/register-form.component.html @@ -0,0 +1,121 @@ +
+
+
+ + {{ "emailAddress" | i18n }} + + {{ "emailAddressDesc" | i18n }} + +
+ +
+ + {{ "name" | i18n }} + + {{ "yourNameDesc" | i18n }} + +
+ +
+ + + + {{ "masterPass" | i18n }} + + + + Important: + {{ "masterPassImportant" | i18n }} + + + + +
+ +
+ + {{ "reTypeMasterPass" | i18n }} + + + +
+ +
+ + {{ "masterPassHint" | i18n }} + + {{ "masterPassHintDesc" | i18n }} + +
+ +
+ +
+ +
+
+ +
+ + {{ "acceptPolicies" | i18n }}
+ {{ + "termsOfService" | i18n + }}, + {{ + "privacyPolicy" | i18n + }} +
+
+ +
+ {{ "createAccount" | i18n }} + + + {{ "logIn" | i18n }} + +
+ +
+
diff --git a/apps/web/src/app/modules/register-form/register-form.component.ts b/apps/web/src/app/modules/register-form/register-form.component.ts new file mode 100644 index 0000000000..519a185fa1 --- /dev/null +++ b/apps/web/src/app/modules/register-form/register-form.component.ts @@ -0,0 +1,87 @@ +import { Component, Input } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { Router } from "@angular/router"; + +import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthService } from "@bitwarden/common/abstractions/auth.service"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; +import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { PolicyService } from "@bitwarden/common/abstractions/policy.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions"; + +@Component({ + selector: "app-register-form", + templateUrl: "./register-form.component.html", +}) +export class RegisterFormComponent extends BaseRegisterComponent { + @Input() queryParamEmail: string; + @Input() enforcedPolicyOptions: MasterPasswordPolicyOptions; + + showErrorSummary = false; + + constructor( + formValidationErrorService: FormValidationErrorsService, + formBuilder: FormBuilder, + authService: AuthService, + router: Router, + i18nService: I18nService, + cryptoService: CryptoService, + apiService: ApiService, + stateService: StateService, + platformUtilsService: PlatformUtilsService, + passwordGenerationService: PasswordGenerationService, + private policyService: PolicyService, + environmentService: EnvironmentService, + logService: LogService + ) { + super( + formValidationErrorService, + formBuilder, + authService, + router, + i18nService, + cryptoService, + apiService, + stateService, + platformUtilsService, + passwordGenerationService, + environmentService, + logService + ); + } + + async ngOnInit() { + await super.ngOnInit(); + + if (this.queryParamEmail) { + this.formGroup.get("email")?.setValue(this.queryParamEmail); + } + } + + async submit() { + if ( + this.enforcedPolicyOptions != null && + !this.policyService.evaluateMasterPassword( + this.masterPasswordScore, + this.formGroup.get("masterPassword")?.value, + this.enforcedPolicyOptions + ) + ) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPasswordPolicyRequirementsNotMet") + ); + return; + } + + await super.submit(false); + } +} diff --git a/apps/web/src/app/modules/register-form/register-form.module.ts b/apps/web/src/app/modules/register-form/register-form.module.ts new file mode 100644 index 0000000000..0c6c919ef1 --- /dev/null +++ b/apps/web/src/app/modules/register-form/register-form.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../shared.module"; + +import { RegisterFormComponent } from "./register-form.component"; + +@NgModule({ + imports: [SharedModule], + declarations: [RegisterFormComponent], + exports: [RegisterFormComponent], +}) +export class RegisterFormModule {} diff --git a/apps/web/src/app/modules/shared.module.ts b/apps/web/src/app/modules/shared.module.ts index a8801e8196..1bfbe63291 100644 --- a/apps/web/src/app/modules/shared.module.ts +++ b/apps/web/src/app/modules/shared.module.ts @@ -61,10 +61,13 @@ import { BadgeModule, ButtonModule, CalloutModule, - MenuModule, + FormFieldModule, SubmitButtonModule, + MenuModule, } from "@bitwarden/components"; +import { PasswordStrengthComponent } from "../components/password-strength.component"; + registerLocaleData(localeAf, "af"); registerLocaleData(localeAz, "az"); registerLocaleData(localeBe, "be"); @@ -117,6 +120,7 @@ registerLocaleData(localeZhCn, "zh-CN"); registerLocaleData(localeZhTw, "zh-TW"); @NgModule({ + declarations: [PasswordStrengthComponent], imports: [ CommonModule, DragDropModule, @@ -132,6 +136,7 @@ registerLocaleData(localeZhTw, "zh-TW"); BadgeModule, ButtonModule, MenuModule, + FormFieldModule, SubmitButtonModule, ], exports: [ @@ -149,6 +154,8 @@ registerLocaleData(localeZhTw, "zh-TW"); BadgeModule, ButtonModule, MenuModule, + FormFieldModule, + PasswordStrengthComponent, SubmitButtonModule, ], providers: [DatePipe], diff --git a/apps/web/src/app/modules/trial-initiation/enterprise-content.component.html b/apps/web/src/app/modules/trial-initiation/enterprise-content.component.html new file mode 100644 index 0000000000..2da9a72cab --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/enterprise-content.component.html @@ -0,0 +1,11 @@ +

You've chosen Bitwarden for Enterprise

+
+

What you can do with Bitwarden for Enterprise

+
+ +
+

Collaborate and share securely

+

Deploy and manage quickly and easily

+

Access anywhere on any device

+

Create your account to get started

+
diff --git a/apps/web/src/app/modules/trial-initiation/enterprise-content.component.ts b/apps/web/src/app/modules/trial-initiation/enterprise-content.component.ts new file mode 100644 index 0000000000..847b3c3088 --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/enterprise-content.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-enterprise-content", + templateUrl: "enterprise-content.component.html", +}) +export class EnterpriseContentComponent {} diff --git a/apps/web/src/app/modules/trial-initiation/families-content.component.html b/apps/web/src/app/modules/trial-initiation/families-content.component.html new file mode 100644 index 0000000000..d2fc304e9e --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/families-content.component.html @@ -0,0 +1,13 @@ +

You've chosen Bitwarden for Families

+
+

+ Trusted by millions of individuals, teams, and organizations worldwide for secure password + storage and sharing. +

+
+
+

Collaborate and share securely

+

Deploy and manage quickly and easily

+

Access anywhere on any device

+

Create your account to get started

+
diff --git a/apps/web/src/app/modules/trial-initiation/families-content.component.ts b/apps/web/src/app/modules/trial-initiation/families-content.component.ts new file mode 100644 index 0000000000..1a13be80e6 --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/families-content.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-families-content", + templateUrl: "families-content.component.html", +}) +export class FamiliesContentComponent {} diff --git a/apps/web/src/app/modules/trial-initiation/teams-content.component.html b/apps/web/src/app/modules/trial-initiation/teams-content.component.html new file mode 100644 index 0000000000..6183618dee --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/teams-content.component.html @@ -0,0 +1,10 @@ +

You've chosen Bitwarden for Teams

+
+

What you can do with Btiwarden for Teams

+
+
+

Collaborate and share securely

+

Deploy and manage quickly and easily

+

Access anywhere on any device

+

Create your account to get started

+
diff --git a/apps/web/src/app/modules/trial-initiation/teams-content.component.ts b/apps/web/src/app/modules/trial-initiation/teams-content.component.ts new file mode 100644 index 0000000000..5c97695def --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/teams-content.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-teams-content", + templateUrl: "teams-content.component.html", +}) +export class TeamsContentComponent {} 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 new file mode 100644 index 0000000000..7e6be8485c --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/trial-initiation.component.html @@ -0,0 +1,58 @@ +
+ +
+
+ Bitwarden + +
+ + + + + + +
+
+
+
+
+
+

+ Start your 7-Day free trial of Bitwarden for {{ org }} +

+
+ + + + +

This is content of "Step 1" that has editable set to false

+ +
+ + +

This is content of "Step 2"

+ +
+ + +

This is content of "Step 3"

+ + +
+ + +

This is any content of "Step 4"

+ +
+
+
+
+
+
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 new file mode 100644 index 0000000000..b3b00f1771 --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/trial-initiation.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { first } from "rxjs"; + +@Component({ + selector: "app-trial", + templateUrl: "trial-initiation.component.html", +}) +export class TrialInitiationComponent implements OnInit { + email = ""; + org = "teams"; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.queryParams.pipe(first()).subscribe((qParams) => { + if (qParams.email != null && qParams.email.indexOf("@") > -1) { + this.email = qParams.email; + } + if (qParams.org) { + this.org = qParams.org; + } + }); + } +} 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 new file mode 100644 index 0000000000..35370acb50 --- /dev/null +++ b/apps/web/src/app/modules/trial-initiation/trial-initiation.module.ts @@ -0,0 +1,24 @@ +import { CdkStepperModule } from "@angular/cdk/stepper"; +import { NgModule } from "@angular/core"; + +import { FormFieldModule } from "@bitwarden/components"; + +import { SharedModule } from "../shared.module"; +import { VerticalStepperModule } from "../vertical-stepper/vertical-stepper.module"; + +import { EnterpriseContentComponent } from "./enterprise-content.component"; +import { FamiliesContentComponent } from "./families-content.component"; +import { TeamsContentComponent } from "./teams-content.component"; +import { TrialInitiationComponent } from "./trial-initiation.component"; + +@NgModule({ + imports: [SharedModule, CdkStepperModule, VerticalStepperModule, FormFieldModule], + declarations: [ + TrialInitiationComponent, + EnterpriseContentComponent, + FamiliesContentComponent, + TeamsContentComponent, + ], + exports: [TrialInitiationComponent], +}) +export class TrialInitiationModule {} diff --git a/apps/web/src/app/modules/vertical-stepper/vertical-step-content.component.html b/apps/web/src/app/modules/vertical-stepper/vertical-step-content.component.html new file mode 100644 index 0000000000..91e214097d --- /dev/null +++ b/apps/web/src/app/modules/vertical-stepper/vertical-step-content.component.html @@ -0,0 +1,45 @@ +
+ +
diff --git a/apps/web/src/app/modules/vertical-stepper/vertical-step-content.component.ts b/apps/web/src/app/modules/vertical-stepper/vertical-step-content.component.ts new file mode 100644 index 0000000000..8a074073db --- /dev/null +++ b/apps/web/src/app/modules/vertical-stepper/vertical-step-content.component.ts @@ -0,0 +1,20 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { VerticalStep } from "./vertical-step.component"; + +@Component({ + selector: "app-vertical-step-content", + templateUrl: "vertical-step-content.component.html", +}) +export class VerticalStepContentComponent { + @Output() onSelectStep = new EventEmitter(); + + @Input() disabled = false; + @Input() selected = false; + @Input() step: VerticalStep; + @Input() stepNumber: number; + + selectStep() { + this.onSelectStep.emit(); + } +} diff --git a/apps/web/src/app/modules/vertical-stepper/vertical-step.component.html b/apps/web/src/app/modules/vertical-stepper/vertical-step.component.html new file mode 100644 index 0000000000..8d963d6c55 --- /dev/null +++ b/apps/web/src/app/modules/vertical-stepper/vertical-step.component.html @@ -0,0 +1,7 @@ + +
+ +
+
diff --git a/apps/web/src/app/modules/vertical-stepper/vertical-step.component.ts b/apps/web/src/app/modules/vertical-stepper/vertical-step.component.ts new file mode 100644 index 0000000000..42c0eca5e8 --- /dev/null +++ b/apps/web/src/app/modules/vertical-stepper/vertical-step.component.ts @@ -0,0 +1,11 @@ +import { CdkStep } from "@angular/cdk/stepper"; +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "app-vertical-step", + templateUrl: "vertical-step.component.html", + providers: [{ provide: CdkStep, useExisting: VerticalStep }], +}) +export class VerticalStep extends CdkStep { + @Input() subLabel = ""; +} diff --git a/apps/web/src/app/modules/vertical-stepper/vertical-stepper.component.html b/apps/web/src/app/modules/vertical-stepper/vertical-stepper.component.html new file mode 100644 index 0000000000..2051cfb47d --- /dev/null +++ b/apps/web/src/app/modules/vertical-stepper/vertical-stepper.component.html @@ -0,0 +1,22 @@ +
+
    +
  • + +
    +
    +
  • +
+
diff --git a/apps/web/src/app/modules/vertical-stepper/vertical-stepper.component.ts b/apps/web/src/app/modules/vertical-stepper/vertical-stepper.component.ts new file mode 100644 index 0000000000..2c66dae7be --- /dev/null +++ b/apps/web/src/app/modules/vertical-stepper/vertical-stepper.component.ts @@ -0,0 +1,29 @@ +import { CdkStepper } from "@angular/cdk/stepper"; +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "app-vertical-stepper", + templateUrl: "vertical-stepper.component.html", + providers: [{ provide: CdkStepper, useExisting: VerticalStepperComponent }], +}) +export class VerticalStepperComponent extends CdkStepper { + @Input() + activeClass = "active"; + + isNextButtonHidden() { + return !(this.steps.length === this.selectedIndex + 1); + } + + isStepDisabled(index: number) { + if (this.selectedIndex !== index) { + return this.selectedIndex === index - 1 + ? !this.steps.find((_, i) => i == index - 1)?.completed + : true; + } + return false; + } + + selectStepByIndex(index: number): void { + this.selectedIndex = index; + } +} diff --git a/apps/web/src/app/modules/vertical-stepper/vertical-stepper.module.ts b/apps/web/src/app/modules/vertical-stepper/vertical-stepper.module.ts new file mode 100644 index 0000000000..71207aa431 --- /dev/null +++ b/apps/web/src/app/modules/vertical-stepper/vertical-stepper.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../shared.module"; + +import { VerticalStepContentComponent } from "./vertical-step-content.component"; +import { VerticalStep } from "./vertical-step.component"; +import { VerticalStepperComponent } from "./vertical-stepper.component"; + +@NgModule({ + imports: [SharedModule], + declarations: [VerticalStepperComponent, VerticalStep, VerticalStepContentComponent], + exports: [VerticalStepperComponent, VerticalStep], +}) +export class VerticalStepperModule {} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 8b2e6a6f92..a98cbad1d2 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -24,6 +24,7 @@ import { VerifyRecoverDeleteComponent } from "./accounts/verify-recover-delete.c import { HomeGuard } from "./guards/home.guard"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; +import { TrialInitiationComponent } from "./modules/trial-initiation/trial-initiation.component"; import { IndividualVaultModule } from "./modules/vault/modules/individual-vault/individual-vault.module"; import { OrganizationsRoutingModule } from "./organizations/organization-routing.module"; import { AcceptFamilySponsorshipComponent } from "./organizations/sponsorships/accept-family-sponsorship.component"; @@ -64,6 +65,12 @@ const routes: Routes = [ canActivate: [UnauthGuard], data: { titleId: "createAccount" }, }, + { + path: "trial", + component: TrialInitiationComponent, + canActivate: [UnauthGuard], + data: { titleId: "startTrial" }, + }, { path: "sso", component: SsoComponent, diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 6a0d27af1a..af19fa08d0 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -5,6 +5,7 @@ import { OrganizationManageModule } from "./modules/organizations/manage/organiz import { OrganizationUserModule } from "./modules/organizations/users/organization-user.module"; import { PipesModule } from "./modules/pipes/pipes.module"; import { SharedModule } from "./modules/shared.module"; +import { TrialInitiationModule } from "./modules/trial-initiation/trial-initiation.module"; import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module"; import { OrganizationBadgeModule } from "./modules/vault/modules/organization-badge/organization-badge.module"; @@ -12,6 +13,7 @@ import { OrganizationBadgeModule } from "./modules/vault/modules/organization-ba imports: [ SharedModule, LooseComponentsModule, + TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, PipesModule, @@ -21,6 +23,7 @@ import { OrganizationBadgeModule } from "./modules/vault/modules/organization-ba exports: [ SharedModule, LooseComponentsModule, + TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, PipesModule, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0e32e2dd53..9d54a00450 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -572,6 +572,9 @@ "createAccount": { "message": "Create Account" }, + "startTrial": { + "message": "Start Trial" + }, "logIn": { "message": "Log In" }, @@ -593,6 +596,9 @@ "masterPassDesc": { "message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it." }, + "masterPassImportant": { + "message": "Master passwords cannot be recovered if you forget it!" + }, "masterPassHintDesc": { "message": "A master password hint can help you remember your password if you forget it." }, @@ -623,10 +629,13 @@ "invalidEmail": { "message": "Invalid email address." }, - "masterPassRequired": { + "masterPasswordRequired": { "message": "Master password is required." }, - "masterPassLength": { + "confirmMasterPasswordRequired": { + "message": "Master password retype is required." + }, + "masterPasswordMinLength": { "message": "Master password must be at least 8 characters long." }, "masterPassDoesntMatch": { @@ -3159,7 +3168,7 @@ "acceptPolicies": { "message": "By checking this box you agree to the following:" }, - "acceptPoliciesError": { + "acceptPoliciesRequired": { "message": "Terms of Service and Privacy Policy have not been acknowledged." }, "termsOfService": { @@ -5165,5 +5174,29 @@ "example": "My Email" } } + }, + "inputRequired": { + "message": "Input is required." + }, + "inputEmail": { + "message": "Input is not an email-address." + }, + "inputMinLength": { + "message": "Input must be at least $COUNT$ characters long.", + "placeholders": { + "count": { + "content": "$1", + "example": "8" + } + } + }, + "fieldsNeedAttention": { + "message": "$COUNT$ field(s) above need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "4" + } + } } } diff --git a/libs/angular/src/components/register.component.ts b/libs/angular/src/components/register.component.ts index 3f5339c916..15694ec838 100644 --- a/libs/angular/src/components/register.component.ts +++ b/libs/angular/src/components/register.component.ts @@ -1,10 +1,19 @@ import { Directive, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; +import { + validateInputsDoesntMatch, + validateInputsMatch, +} from "@bitwarden/angular/validators/fieldsInputCheck.validator"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; +import { + AllValidationErrors, + FormValidationErrorsService, +} from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; @@ -19,22 +28,38 @@ import { CaptchaProtectedComponent } from "./captchaProtected.component"; @Directive() export class RegisterComponent extends CaptchaProtectedComponent implements OnInit { - name = ""; - email = ""; - masterPassword = ""; - confirmMasterPassword = ""; - hint = ""; showPassword = false; formPromise: Promise; masterPasswordScore: number; referenceData: ReferenceEventRequest; showTerms = true; - acceptPolicies = false; + showErrorSummary = false; + + formGroup = this.formBuilder.group({ + email: ["", [Validators.required, Validators.email]], + name: [""], + masterPassword: ["", [Validators.required, Validators.minLength(8)]], + confirmMasterPassword: [ + "", + [ + Validators.required, + Validators.minLength(8), + validateInputsMatch("masterPassword", this.i18nService.t("masterPassDoesntMatch")), + ], + ], + hint: [ + null, + [validateInputsDoesntMatch("masterPassword", this.i18nService.t("hintEqualsPassword"))], + ], + acceptPolicies: [false, [Validators.requiredTrue]], + }); protected successRoute = "login"; private masterPasswordStrengthTimeout: any; constructor( + protected formValidationErrorService: FormValidationErrorsService, + protected formBuilder: FormBuilder, protected authService: AuthService, protected router: Router, i18nService: I18nService, @@ -84,59 +109,38 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } } - async submit() { - if (!this.acceptPolicies && this.showTerms) { + async submit(showToast = true) { + let email = this.formGroup.get("email")?.value; + let name = this.formGroup.get("name")?.value; + const masterPassword = this.formGroup.get("masterPassword")?.value; + const hint = this.formGroup.get("hint")?.value; + + this.formGroup.markAllAsTouched(); + this.showErrorSummary = true; + + if (this.formGroup.get("acceptPolicies").hasError("required")) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), - this.i18nService.t("acceptPoliciesError") + this.i18nService.t("acceptPoliciesRequired") ); return; } - if (this.email == null || this.email === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("emailRequired") - ); + //web + if (this.formGroup.invalid && !showToast) { return; } - if (this.email.indexOf("@") === -1) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidEmail") - ); - return; - } - if (this.masterPassword == null || this.masterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPassRequired") - ); - return; - } - if (this.masterPassword.length < 8) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPassLength") - ); - return; - } - if (this.masterPassword !== this.confirmMasterPassword) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPassDoesntMatch") - ); + + //desktop, browser + if (this.formGroup.invalid && showToast) { + const errorText = this.getErrorToastMessage(); + this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText); return; } const strengthResult = this.passwordGenerationService.passwordStrength( - this.masterPassword, + masterPassword, this.getPasswordStrengthUserInput() ); if (strengthResult != null && strengthResult.score < 3) { @@ -152,33 +156,19 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } } - if (this.hint === this.masterPassword) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("hintEqualsPassword") - ); - return; - } - - this.name = this.name === "" ? null : this.name; - this.email = this.email.trim().toLowerCase(); + name = name === "" ? null : name; + email = email.trim().toLowerCase(); const kdf = DEFAULT_KDF_TYPE; const kdfIterations = DEFAULT_KDF_ITERATIONS; - const key = await this.cryptoService.makeKey( - this.masterPassword, - this.email, - kdf, - kdfIterations - ); + const key = await this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); const encKey = await this.cryptoService.makeEncKey(key); - const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); + const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); const keys = await this.cryptoService.makeKeyPair(encKey[0]); const request = new RegisterRequest( - this.email, - this.name, + email, + name, hashedPassword, - this.hint, + hint, encKey[1].encryptedString, kdf, kdfIterations, @@ -204,24 +194,25 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } } this.platformUtilsService.showToast("success", null, this.i18nService.t("newAccountCreated")); - this.router.navigate([this.successRoute], { queryParams: { email: this.email } }); + this.router.navigate([this.successRoute], { queryParams: { email: email } }); } catch (e) { this.logService.error(e); } } - togglePassword(confirmField: boolean) { + togglePassword() { this.showPassword = !this.showPassword; - document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus(); } updatePasswordStrength() { + const masterPassword = this.formGroup.get("masterPassword")?.value; + if (this.masterPasswordStrengthTimeout != null) { clearTimeout(this.masterPasswordStrengthTimeout); } this.masterPasswordStrengthTimeout = setTimeout(() => { const strengthResult = this.passwordGenerationService.passwordStrength( - this.masterPassword, + masterPassword, this.getPasswordStrengthUserInput() ); this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; @@ -230,19 +221,47 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn private getPasswordStrengthUserInput() { let userInput: string[] = []; - const atPosition = this.email.indexOf("@"); + const email = this.formGroup.get("email")?.value; + const name = this.formGroup.get("name").value; + const atPosition = email.indexOf("@"); if (atPosition > -1) { userInput = userInput.concat( - this.email + email .substr(0, atPosition) .trim() .toLowerCase() .split(/[^A-Za-z0-9]/) ); } - if (this.name != null && this.name !== "") { - userInput = userInput.concat(this.name.trim().toLowerCase().split(" ")); + if (name != null && name !== "") { + userInput = userInput.concat(name.trim().toLowerCase().split(" ")); } return userInput; } + + private getErrorToastMessage() { + const error: AllValidationErrors = this.formValidationErrorService + .getFormValidationErrors(this.formGroup.controls) + .shift(); + + if (error) { + switch (error.errorName) { + case "email": + return this.i18nService.t("invalidEmail"); + case "inputsDoesntMatchError": + return this.i18nService.t("masterPassDoesntMatch"); + case "inputsMatchError": + return this.i18nService.t("hintEqualsPassword"); + default: + return this.i18nService.t(this.errorTag(error)); + } + } + + return; + } + + private errorTag(error: AllValidationErrors): string { + const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1); + return `${error.controlName}${name}`; + } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 1314abf4da..245fa58bf9 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -17,6 +17,7 @@ import { EventService as EventServiceAbstraction } from "@bitwarden/common/abstr import { ExportService as ExportServiceAbstraction } from "@bitwarden/common/abstractions/export.service"; import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/abstractions/fileUpload.service"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/abstractions/folder.service"; +import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/abstractions/keyConnector.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -58,6 +59,7 @@ import { EventService } from "@bitwarden/common/services/event.service"; import { ExportService } from "@bitwarden/common/services/export.service"; import { FileUploadService } from "@bitwarden/common/services/fileUpload.service"; import { FolderService } from "@bitwarden/common/services/folder.service"; +import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service"; import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { OrganizationService } from "@bitwarden/common/services/organization.service"; @@ -444,6 +446,10 @@ export const LOG_MAC_FAILURES = new InjectionToken("LOG_MAC_FAILURES"); provide: AbstractThemingService, useClass: ThemingService, }, + { + provide: FormValidationErrorsServiceAbstraction, + useClass: FormValidationErrorsService, + }, ], }) export class JslibServicesModule {} diff --git a/libs/angular/src/validators/fieldsInputCheck.validator.ts b/libs/angular/src/validators/fieldsInputCheck.validator.ts new file mode 100644 index 0000000000..ac1b4d5a40 --- /dev/null +++ b/libs/angular/src/validators/fieldsInputCheck.validator.ts @@ -0,0 +1,37 @@ +import { AbstractControl, ValidatorFn } from "@angular/forms"; + +import { FormGroupControls } from "@bitwarden/common/abstractions/formValidationErrors.service"; + +//check to ensure two fields do not have the same value +export function validateInputsDoesntMatch(matchTo: string, errorMessage: string): ValidatorFn { + return (control: AbstractControl) => { + if (control.parent && control.parent.controls) { + return control?.value === (control?.parent?.controls as FormGroupControls)[matchTo].value + ? { + inputsMatchError: { + message: errorMessage, + }, + } + : null; + } + + return null; + }; +} + +//check to ensure two fields have the same value +export function validateInputsMatch(matchTo: string, errorMessage: string): ValidatorFn { + return (control: AbstractControl) => { + if (control.parent && control.parent.controls) { + return control?.value === (control?.parent?.controls as FormGroupControls)[matchTo].value + ? null + : { + inputsDoesntMatchError: { + message: errorMessage, + }, + }; + } + + return null; + }; +} diff --git a/libs/common/src/abstractions/formValidationErrors.service.ts b/libs/common/src/abstractions/formValidationErrors.service.ts new file mode 100644 index 0000000000..08a12443a0 --- /dev/null +++ b/libs/common/src/abstractions/formValidationErrors.service.ts @@ -0,0 +1,13 @@ +import { AbstractControl } from "@angular/forms"; +export interface AllValidationErrors { + controlName: string; + errorName: string; +} + +export interface FormGroupControls { + [key: string]: AbstractControl; +} + +export abstract class FormValidationErrorsService { + getFormValidationErrors: (controls: FormGroupControls) => AllValidationErrors[]; +} diff --git a/libs/common/src/services/formValidationErrors.service.ts b/libs/common/src/services/formValidationErrors.service.ts new file mode 100644 index 0000000000..c5ce5377eb --- /dev/null +++ b/libs/common/src/services/formValidationErrors.service.ts @@ -0,0 +1,31 @@ +import { FormGroup, ValidationErrors } from "@angular/forms"; + +import { + FormGroupControls, + FormValidationErrorsService as FormValidationErrorsAbstraction, + AllValidationErrors, +} from "../abstractions/formValidationErrors.service"; + +export class FormValidationErrorsService implements FormValidationErrorsAbstraction { + getFormValidationErrors(controls: FormGroupControls): AllValidationErrors[] { + let errors: AllValidationErrors[] = []; + Object.keys(controls).forEach((key) => { + const control = controls[key]; + if (control instanceof FormGroup) { + errors = errors.concat(this.getFormValidationErrors(control.controls)); + } + + const controlErrors: ValidationErrors = controls[key].errors; + if (controlErrors !== null) { + Object.keys(controlErrors).forEach((keyError) => { + errors.push({ + controlName: key, + errorName: keyError, + }); + }); + } + }); + + return errors; + } +} diff --git a/libs/components/src/form-field/error.component.ts b/libs/components/src/form-field/error.component.ts index 26c1eec78f..5cf95545e2 100644 --- a/libs/components/src/form-field/error.component.ts +++ b/libs/components/src/form-field/error.component.ts @@ -26,6 +26,8 @@ export class BitErrorComponent { return this.i18nService.t("inputRequired"); case "email": return this.i18nService.t("inputEmail"); + case "minlength": + return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength); default: // Attempt to show a custom error message. if (this.error[1]?.message) {