1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-23 11:43:46 +00:00

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
38 changed files with 834 additions and 314 deletions

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,