Merge Feature/trial initiation (#3036)

* [SG-74] Trial Initiation Component with Vertical Stepper (#2913)

* Vertical stepper PoC

* Convert stepper css to tailwind

* trial component start

* trial component params

* tailwind-ify header

* Support teams, enterprise, and families layout param and more layout ui work

* Some more theming fixes

* Rename TrialModule to TrialInitiationModule

* Stepper fixes, plus more functionality demo

* Cleanup

* layout params and placeholders

* Only allow trial route to be hit if not logged in

* fix typo

* Use background-alt2 color for header

* Move vertical stepper out of trial-initiation

* Create components for the different plan types

* Remove width on steps

* Remove content projection for label

* Tailwind style fixes

* Extract step content into a component

* Remove layout param for now

* Remove step tags

* remove pointer classes from step button

* Remove most tailwind important designations

* Update apps/web/src/app/modules/vertical-stepper/vertical-step.component.ts

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Tailwind and layout fixes

* Remove container

* lint & prettier fixes

* Remove extra CdkStep declaration

* Styles fixes

* Style logo directly

* Remove 0 margin on image

* Fix tiling and responsiveness

* Minor padding fixes for org pages

* Update apps/web/src/app/modules/trial-initiation/trial-initiation.component.html

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* prettier fix

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* [SG-65] Reusable Registration Form (#2946)

* created reusable registration form

* fixed conflicts

* replicated reactive form changes in other clients

* removed comments

* client template cleanup

* client template cleanup

* removed comments in template file

* changed to component suffix

* switched show password to use component

* comments resolution

* comments resolution

* added toast disable functionality

* removed unused locale

* mode custom input validator generic

* fixed button

* fixed linter

* removed horizontal rule

* switched to button component

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
Co-authored-by: gbubemismith <gsmithwalter@gmail.com>
This commit is contained in:
Robyn MacCallum 2022-07-05 15:25:14 -04:00 committed by GitHub
parent 9d1312f2af
commit fb70d8a2d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 834 additions and 314 deletions

View File

@ -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": {

View File

@ -1,4 +1,4 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
<header>
<div class="left">
<a routerLink="/home">{{ "cancel" | i18n }}</a>
@ -18,16 +18,7 @@
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
inputmode="email"
appInputVerbatim="false"
/>
<input id="email" type="email" formControlName="email" appInputVerbatim="false" />
</div>
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
@ -44,11 +35,8 @@
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
[appAutofocus]="email !== ''"
formControlName="masterPassword"
appInputVerbatim
(input)="updatePasswordStrength()"
/>
@ -60,7 +48,7 @@
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
(click)="togglePassword()"
[attr.aria-pressed]="showPassword"
>
<i
@ -96,10 +84,8 @@
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="confirmMasterPassword"
required
formControlName="confirmMasterPassword"
appInputVerbatim
/>
</div>
@ -110,7 +96,7 @@
appStopClick
appBlurClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
(click)="togglePassword()"
[attr.aria-pressed]="showPassword"
>
<i
@ -123,7 +109,7 @@
</div>
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
<input id="hint" type="text" formControlName="hint" />
</div>
</div>
<div class="box-footer">
@ -137,12 +123,7 @@
class="box-content-row box-content-row-checkbox box-content-row-checkbox-left box-content-row-word-break"
appBoxRow
>
<input
type="checkbox"
id="acceptPolicies"
[(ngModel)]="acceptPolicies"
name="AcceptPolicies"
/>
<input type="checkbox" id="acceptPolicies" formControlName="acceptPolicies" />
<label for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{

View File

@ -1,4 +1,5 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component";
@ -6,6 +7,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";
@ -18,6 +20,8 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
})
export class RegisterComponent extends BaseRegisterComponent {
constructor(
formValidationErrorService: FormValidationErrorsService,
formBuilder: FormBuilder,
authService: AuthService,
router: Router,
i18nService: I18nService,
@ -30,6 +34,8 @@ export class RegisterComponent extends BaseRegisterComponent {
logService: LogService
) {
super(
formValidationErrorService,
formBuilder,
authService,
router,
i18nService,

View File

@ -1,19 +1,17 @@
<form id="register-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<form
id="register-page"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="formGroup"
>
<div class="content">
<h1>{{ "createAccount" | i18n }}</h1>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
appInputVerbatim
/>
<input id="email" type="email" formControlName="email" appInputVerbatim />
</div>
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
@ -30,11 +28,8 @@
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
[appAutofocus]="email !== ''"
formControlName="masterPassword"
(input)="updatePasswordStrength()"
appInputVerbatim
/>
@ -46,7 +41,7 @@
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
@ -81,10 +76,8 @@
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="confirmMasterPassword"
required
formControlName="confirmMasterPassword"
appInputVerbatim
/>
</div>
@ -95,7 +88,7 @@
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
@ -107,7 +100,7 @@
</div>
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
<input id="hint" type="text" formControlName="hint" />
</div>
<div class="box last" [hidden]="!showCaptcha()">
<div class="box-content">
@ -127,12 +120,7 @@
</div>
<div class="box last" *ngIf="showTerms">
<div class="box-footer checkbox">
<input
type="checkbox"
id="acceptPolicies"
[(ngModel)]="acceptPolicies"
name="AcceptPolicies"
/>
<input type="checkbox" id="acceptPolicies" formControlName="acceptPolicies" />
<label for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{

View File

@ -1,4 +1,5 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component";
@ -7,6 +8,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.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";
@ -21,6 +23,8 @@ const BroadcasterSubscriptionId = "RegisterComponent";
})
export class RegisterComponent extends BaseRegisterComponent implements OnInit, OnDestroy {
constructor(
formValidationErrorService: FormValidationErrorsService,
formBuilder: FormBuilder,
authService: AuthService,
router: Router,
i18nService: I18nService,
@ -35,6 +39,8 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit,
logService: LogService
) {
super(
formValidationErrorService,
formBuilder,
authService,
router,
i18nService,

View File

@ -532,10 +532,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": {
@ -1543,7 +1546,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."
},
"enableBrowserIntegration": {

View File

@ -28,7 +28,7 @@
</div>
</header>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="container">
<div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
@ -112,156 +112,10 @@
>
{{ "createOrganizationCreatePersonalAccount" | i18n }}
</app-callout>
<div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
inputmode="email"
appInputVerbatim="false"
/>
<small class="form-text text-muted">{{ "emailAddressDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="name">{{ "yourName" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="name"
[appAutofocus]="email !== ''"
/>
<small class="form-text text-muted">{{ "yourNameDesc" | i18n }}</small>
</div>
<div class="form-group">
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<div class="w-100">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required
appInputVerbatim
/>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{
'bwi-eye': !showPassword,
'bwi-eye-slash': showPassword
}"
></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{ "masterPassDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input
id="hint"
class="form-control"
type="text"
name="Hint"
[(ngModel)]="hint"
/>
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="form-group" *ngIf="showTerms">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="acceptPolicies"
[(ngModel)]="acceptPolicies"
name="AcceptPolicies"
/>
<label class="form-check-label small text-muted" for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</label>
</div>
</div>
<hr />
<div class="d-flex mb-2">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>{{ "submit" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
<app-register-form
[queryParamEmail]="email"
[enforcedPolicyOptions]="enforcedPolicyOptions"
></app-register-form>
</div>
</div>
</div>
@ -351,5 +205,5 @@
/>
</div>
</div>
</form>
</div>
</div>

View File

@ -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();
}
}

View File

@ -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,

View File

@ -0,0 +1,121 @@
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
class="tw-container tw-mx-auto"
[formGroup]="formGroup"
>
<div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
<input bitInput type="email" formControlName="email" />
<bit-hint>{{ "emailAddressDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
<bit-hint>{{ "yourNameDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3">
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
bitInput
(input)="updatePasswordStrength()"
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="masterPassword"
/>
<button type="button" bitSuffix bitButton (click)="togglePassword()">
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<bit-hint>
<span class="tw-font-semibold">Important:</span>
{{ "masterPassImportant" | i18n }}
</bit-hint>
</bit-form-field>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
<input
bitInput
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="confirmMasterPassword"
/>
<button type="button" bitSuffix bitButton (click)="togglePassword()">
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "masterPassHint" | i18n }}</bit-label>
<input bitInput type="text" formControlName="hint" />
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="tw-flex tw-items-start tw-mb-3" *ngIf="showTerms">
<div class="tw-flex tw-items-center tw-h-6">
<input
class="tw-w-4 tw-rounded tw-border tw-border-gray-300"
bitInput
type="checkbox"
formControlName="acceptPolicies"
/>
</div>
<bit-label class="ml-2">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</bit-label>
</div>
<div class="tw-flex tw-mb-3">
<bit-submit-button [loading]="form.loading">{{ "createAccount" | i18n }}</bit-submit-button>
<a
bitButton
buttonType="secondary"
routerLink="/login"
class="tw-inline-flex tw-items-center tw-ml-3 tw-px-3"
>
<i class="bwi bwi-sign-in tw-mr-2"></i>
{{ "logIn" | i18n }}
</a>
</div>
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
</div>
</form>

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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],

View File

@ -0,0 +1,11 @@
<h1 class="!tw-text-alt2">You've chosen Bitwarden for Enterprise</h1>
<div class="tw-pt-24">
<h2>What you can do with Bitwarden for Enterprise</h2>
</div>
<div class="tw-text-3xl tw-text-main tw-mt-12">
<p class="tw-mt-2.5 tw-mb-20">Collaborate and share securely</p>
<p class="tw-mt-2.5 tw-mb-20">Deploy and manage quickly and easily</p>
<p class="tw-mt-2.5 tw-mb-20">Access anywhere on any device</p>
<p class="tw-mt-2.5 tw-mb-20">Create your account to get started</p>
</div>

View File

@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise-content",
templateUrl: "enterprise-content.component.html",
})
export class EnterpriseContentComponent {}

View File

@ -0,0 +1,13 @@
<h1 class="!tw-text-alt2">You've chosen Bitwarden for Families</h1>
<div class="tw-pt-24">
<h2>
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<div class="tw-text-3xl tw-text-main tw-mt-12">
<p class="tw-mt-2.5 tw-mb-20">Collaborate and share securely</p>
<p class="tw-mt-2.5 tw-mb-20">Deploy and manage quickly and easily</p>
<p class="tw-mt-2.5 tw-mb-20">Access anywhere on any device</p>
<p class="tw-mt-2.5 tw-mb-20">Create your account to get started</p>
</div>

View File

@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-families-content",
templateUrl: "families-content.component.html",
})
export class FamiliesContentComponent {}

View File

@ -0,0 +1,10 @@
<h1 class="!tw-text-alt2">You've chosen Bitwarden for Teams</h1>
<div class="tw-pt-24">
<h2>What you can do with Btiwarden for Teams</h2>
</div>
<div class="tw-text-3xl tw-text-main tw-mt-12">
<p class="tw-mt-2.5 tw-mb-20">Collaborate and share securely</p>
<p class="tw-mt-2.5 tw-mb-20">Deploy and manage quickly and easily</p>
<p class="tw-mt-2.5 tw-mb-20">Access anywhere on any device</p>
<p class="tw-mt-2.5 tw-mb-20">Create your account to get started</p>
</div>

View File

@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams-content",
templateUrl: "teams-content.component.html",
})
export class TeamsContentComponent {}

View File

@ -0,0 +1,58 @@
<div
class="tw-bg-background-alt2 tw-h-96 tw--mt-48 tw-absolute tw--skew-y-3 tw-w-full tw--z-10"
></div>
<div class="tw-flex tw-max-w-screen-xl tw-min-w-4xl tw-mx-auto tw-px-4">
<div class="tw-w-1/2">
<img
alt="Bitwarden"
style="height: 50px; width: 335px"
class="tw-mt-6"
src="../../images/register-layout/logo-horizontal-white.svg"
/>
<!-- This is to for illustrative purposes and content will be replaced by marketing -->
<div class="tw-pt-12">
<!-- Teams Body -->
<app-teams-content *ngIf="org === 'teams'"></app-teams-content>
<!-- Enterprise Body -->
<app-enterprise-content *ngIf="org === 'enterprise'"></app-enterprise-content>
<!-- Families Body -->
<app-families-content *ngIf="org === 'families'"></app-families-content>
</div>
</div>
<div class="tw-w-1/2">
<div class="tw-pt-56">
<div class="tw-rounded tw-border tw-border-solid tw-bg-background tw-border-secondary-300">
<div class="tw-h-12 tw-flex tw-items-center tw-rounded-t tw-bg-secondary-100 tw-w-full">
<h2 class="tw-uppercase tw-pl-4 tw-text-base tw-mb-0 tw-font-bold">
Start your 7-Day free trial of Bitwarden for {{ org }}
</h2>
</div>
<app-vertical-stepper linear>
<!-- Content is for demo purposes. Replace with form components for each step-->
<app-vertical-step label="Create Account" [editable]="false">
<!-- Replace content with Registration step -->
<p>This is content of "Step 1" that has editable set to false</p>
<button bitButton buttonType="primary" cdkStepperNext>Complete step</button>
</app-vertical-step>
<app-vertical-step label="Create Organization" subLabel="It better be a good org">
<!-- Replace with Org creation step -->
<p>This is content of "Step 2"</p>
<button bitButton buttonType="primary" cdkStepperNext>Complete step</button>
</app-vertical-step>
<app-vertical-step label="Billing">
<!-- Replace with Billing step -->
<p>This is content of "Step 3"</p>
<button bitButton buttonType="secondary" cdkStepperPrevious>Back</button>
<button bitButton buttonType="primary" cdkStepperNext>Complete step</button>
</app-vertical-step>
<app-vertical-step label="Confirmation Details" subLabel="Fancy sub label">
<!-- Replace with Confirmation details step -->
<p>This is any content of "Step 4"</p>
<button bitButton buttonType="primary" cdkStepperNext>Complete</button>
</app-vertical-step>
</app-vertical-stepper>
</div>
</div>
</div>
</div>

View File

@ -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;
}
});
}
}

View File

@ -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 {}

View File

@ -0,0 +1,45 @@
<div class="tw-m-2.5 tw-text-center tw-h-16">
<button
(click)="selectStep()"
[disabled]="disabled"
class="tw-w-full tw-flex tw-border-none tw-bg-transparent tw-items-center"
[ngClass]="{
'hover:tw-bg-secondary-100': !disabled && step.editable
}"
[attr.aria-expanded]="selected"
>
<span
class="tw-rounded-full tw-font-bold tw-leading-9 tw-mr-3.5 tw-w-9"
*ngIf="!step.completed"
[ngClass]="{
'tw-text-contrast tw-bg-primary-500': selected,
'tw-text-main tw-bg-secondary-300': !selected && !disabled && step.editable,
'tw-text-muted tw-bg-transparent': disabled
}"
>
{{ stepNumber }}
</span>
<span
class="tw-text-contrast tw-bg-primary-500 tw-rounded-full tw-font-bold tw-leading-9 tw-mr-3.5 tw-w-9"
*ngIf="step.completed"
>
<i class="bwi bwi-fw bwi-check tw-p-1" aria-hidden="true"></i>
</span>
<div
class="tw-text-left tw-txt-main tw-leading-snug tw-h-12 tw-mt-3.5"
[ngClass]="{
'tw-font-bold': selected
}"
>
<p
class="main-label text tw-text-main tw-mb-1"
[ngClass]="{
'tw-mt-1': !step.subLabel
}"
>
{{ step.label }}
</p>
<p class="sub-label small tw-text-muted">{{ step.subLabel }}</p>
</div>
</button>
</div>

View File

@ -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<void>();
@Input() disabled = false;
@Input() selected = false;
@Input() step: VerticalStep;
@Input() stepNumber: number;
selectStep() {
this.onSelectStep.emit();
}
}

View File

@ -0,0 +1,7 @@
<ng-template>
<div
class="tw-pl-7 tw-inline-block tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300 tw-w-10/12"
>
<ng-content></ng-content>
</div>
</ng-template>

View File

@ -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 = "";
}

View File

@ -0,0 +1,22 @@
<div>
<ul class="tw-flex tw-list-none tw-flex-col tw-flex-wrap tw-p-5">
<li *ngFor="let step of steps; let i = index; let isLast = last">
<app-vertical-step-content
[disabled]="isStepDisabled(i)"
[selected]="selectedIndex === i"
[step]="step"
[stepNumber]="i + 1"
(onSelectStep)="selectStepByIndex(i)"
></app-vertical-step-content>
<div
class="tw-pl-7 tw-inline-block"
*ngIf="selectedIndex === i"
[ngTemplateOutlet]="selected ? selected.content : null"
></div>
<div
class="tw-h-6 tw-ml-8 tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300"
*ngIf="!isLast && !(selectedIndex === i)"
></div>
</li>
</ul>
</div>

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -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,

View File

@ -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,

View File

@ -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"
}
}
}
}

View File

@ -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<any>;
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}`;
}
}

View File

@ -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<string>("LOG_MAC_FAILURES");
provide: AbstractThemingService,
useClass: ThemingService,
},
{
provide: FormValidationErrorsServiceAbstraction,
useClass: FormValidationErrorsService,
},
],
})
export class JslibServicesModule {}

View File

@ -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;
};
}

View File

@ -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[];
}

View File

@ -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;
}
}

View File

@ -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) {