mirror of
https://github.com/bitwarden/browser
synced 2026-01-06 10:33:57 +00:00
[SG-360] Remove the /modules/ folder (#3225)
* Move Web's SharedModule to /app/shared/ This commit relocates `SharedModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `SharedModule`. * Move /modules/pipes to /shared/pipes This commit relocates `PipesModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `PipesModule`. * Move LooseComponentsModule to /shared/ This commit relocates `LooseComponentsModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `LooseComponentsModule`. * Move VerticalStepperModule to /shared/ This commit relocates `VerticalStepperModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `VerticalStepperModule`. * Move TrialInitiationModule to /shared/ This commit relocates `TrialInitiationModule` & `RegisterFormModule` from `/app/modules` to `/app/shared` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference `TrialInitiationModule` or `RegisterFormModule`. * Move /modules/organization to /organization This commit relocates all modules in `/app/modules/organization` to `/app/organization` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Move /modules/vault/ to /vault This commit relocates the IndividualVaultModule to `/app/modules/vault`, and the OrganizationVaultModule to `/app/organization/vault` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Move VaultFiltersModule to /vault This commit relocates the `VaultFilterModule` to `/app/vault/vault-filter`, and the OrganizationVaultFilterComponent to `/app/organization/vault/vault-filter` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Remove the /modules/ folder from desktop This commit relocates the `VaultFilterModule` to `/app/vault/vault-filter`, and the OrganizationVaultFilterComponent to `/app/organization/vault/vault-filter` to align with [ADR #11](https://adr.bitwarden.com/decisions/0011-angular-folder-structure) All other changes are just to adjust imports that reference the moved modules. * Move Libs' VaultFiltersComponent to /vault/ This commit moves the lib's logic for `VaultFiltersModule` from `/modules/` to `/vault/` All other changes are just to adjust imports that reference the moved files. * Rename VaultModule -> SharedVaultModule * Rename IndividualVaultModule -> VaultModule * Rename OrganizationVaultModule -> VaultModule * Rename OrganizationVaultFilterComponent Rename OrganizationVaultFilterComponent to VaultFilterComponent * Seperate the two VaultFilterComponents This commit seperate the `OrganizationVaultFilterComponent` from the `VaultFilerModule`, which is only used by the individual vault. A `VaultFilterSharedModule` was created to declare shared components and provide shared services between the two implementations. This was done to align with best practices for NgModules. * [r] Move VerticalStepperModule to /account/ More specifically, /account/trial/ * [r] Declare PaymentComponent in LooseComponentsModule `PaymentComponent` is not reused across domains and should not be declared in `SharedModule`. I've moved it to `LooseComponentsModule` for now, but later it will need to be exported from a `SettingsModule`. * [r] Declare TaxInfoComponent in LooseComponentsModule * [r] Reloacte Pipes out of /shared/ * [r] Extract locales out of SharedModule * [r] Add documentation to shared module * [r] Cleanup imports * [r] Use an index.ts file for /shared/ * [r] Add eslint rule restricting access to /shared/ Co-authored-by: Hinton <hinton@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
<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 id="register-form_input_email" 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 id="register-form_input_name" 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
|
||||
id="register-form_input_master-password"
|
||||
bitInput
|
||||
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
|
||||
[password]="formGroup.get('masterPassword')?.value"
|
||||
[email]="formGroup.get('email')?.value"
|
||||
[name]="formGroup.get('name')?.value"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</app-password-strength>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_confirm-master-password"
|
||||
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 id="register-form_input_hint" 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-mb-3 tw-flex tw-items-start" *ngIf="showTerms">
|
||||
<div class="tw-flex tw-h-6 tw-items-center">
|
||||
<input
|
||||
id="register-form_input_accept-policies"
|
||||
class="tw-border-gray-300 tw-w-4 tw-rounded tw-border"
|
||||
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-mb-3 tw-flex">
|
||||
<bit-submit-button [loading]="form.loading">{{ "createAccount" | i18n }}</bit-submit-button>
|
||||
<a
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
routerLink="/login"
|
||||
class="tw-ml-3 tw-inline-flex tw-items-center 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>
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { UntypedFormBuilder } 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/policy.service.abstraction";
|
||||
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: UntypedFormBuilder,
|
||||
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.passwordStrengthResult.score,
|
||||
this.formGroup.get("masterPassword")?.value,
|
||||
this.enforcedPolicyOptions
|
||||
)
|
||||
) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPasswordPolicyRequirementsNotMet")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { RegisterFormComponent } from "./register-form.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [RegisterFormComponent],
|
||||
exports: [RegisterFormComponent],
|
||||
})
|
||||
export class RegisterFormModule {}
|
||||
@@ -0,0 +1,48 @@
|
||||
<form #form [formGroup]="formGroup" [appApiAction]="formPromise" (ngSubmit)="submit()">
|
||||
<div class="tw-container tw-mb-3">
|
||||
<div class="tw-mb-6">
|
||||
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "billingPlanLabel" | i18n }}</h2>
|
||||
<div class="tw-mb-1 tw-items-center" *ngFor="let selectablePlan of selectablePlans">
|
||||
<label class="tw- tw-block tw-text-main" for="interval{{ selectablePlan.type }}">
|
||||
<input
|
||||
checked
|
||||
class="tw-h-4 tw-w-4 tw-align-middle"
|
||||
id="interval{{ selectablePlan.type }}"
|
||||
name="plan"
|
||||
type="radio"
|
||||
[value]="selectablePlan.type"
|
||||
formControlName="plan"
|
||||
/>
|
||||
<ng-container *ngIf="selectablePlan.isAnnual">
|
||||
{{ "annual" | i18n }} -
|
||||
{{
|
||||
(selectablePlan.basePrice === 0 ? selectablePlan.seatPrice : selectablePlan.basePrice)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "yr" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
||||
{{ "monthly" | i18n }} -
|
||||
{{
|
||||
(selectablePlan.basePrice === 0 ? selectablePlan.seatPrice : selectablePlan.basePrice)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "monthAbbr" | i18n }}
|
||||
</ng-container>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-4">
|
||||
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "paymentType" | i18n }}</h2>
|
||||
<app-payment [hideCredit]="true" [trialFlow]="true"></app-payment>
|
||||
<app-tax-info [trialFlow]="true" (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-space-x-2">
|
||||
<bit-submit-button [loading]="form.loading">{{ "startTrial" | i18n }}</bit-submit-button>
|
||||
|
||||
<button bitButton type="button" buttonType="secondary" (click)="stepBack()">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { UntypedFormBuilder, FormGroup } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/abstractions/sync.service";
|
||||
import { ProductType } from "@bitwarden/common/enums/productType";
|
||||
|
||||
import { OrganizationPlansComponent } from "../../settings/organization-plans.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-billing",
|
||||
templateUrl: "./billing.component.html",
|
||||
})
|
||||
export class BillingComponent extends OrganizationPlansComponent {
|
||||
@Input() orgInfoForm: FormGroup;
|
||||
@Output() previousStep = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
cryptoService: CryptoService,
|
||||
router: Router,
|
||||
syncService: SyncService,
|
||||
policyService: PolicyService,
|
||||
organizationService: OrganizationService,
|
||||
logService: LogService,
|
||||
messagingService: MessagingService,
|
||||
formBuilder: UntypedFormBuilder
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
cryptoService,
|
||||
router,
|
||||
syncService,
|
||||
policyService,
|
||||
organizationService,
|
||||
logService,
|
||||
messagingService,
|
||||
formBuilder
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const additionalSeats = this.product == ProductType.Families ? 0 : 1;
|
||||
this.formGroup.patchValue({
|
||||
name: this.orgInfoForm.get("name")?.value,
|
||||
billingEmail: this.orgInfoForm.get("email")?.value,
|
||||
additionalSeats: additionalSeats,
|
||||
plan: this.plan,
|
||||
product: this.product,
|
||||
});
|
||||
this.isInTrialFlow = true;
|
||||
await super.ngOnInit();
|
||||
}
|
||||
|
||||
stepBack() {
|
||||
this.previousStep.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="tw-pl-6 tw-pb-6">
|
||||
<p class="tw-text-xl">{{ "trialThankYou" | i18n: orgLabel }}</p>
|
||||
<ul class="tw-list-disc">
|
||||
<li>
|
||||
<p>
|
||||
{{ "trialConfirmationEmail" | i18n }}
|
||||
<span class="tw-font-bold">{{ email }}</span
|
||||
>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
{{ "trialPaidInfoMessage" | i18n: orgLabel }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-trial-confirmation-details",
|
||||
templateUrl: "confirmation-details.component.html",
|
||||
})
|
||||
export class ConfirmationDetailsComponent {
|
||||
@Input() email: string;
|
||||
@Input() orgLabel: string;
|
||||
}
|
||||
@@ -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-mt-12 tw-text-3xl tw-text-main">
|
||||
<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>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-enterprise-content",
|
||||
templateUrl: "enterprise-content.component.html",
|
||||
})
|
||||
export class EnterpriseContentComponent {}
|
||||
@@ -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-mt-12 tw-text-3xl tw-text-main">
|
||||
<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>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-families-content",
|
||||
templateUrl: "families-content.component.html",
|
||||
})
|
||||
export class FamiliesContentComponent {}
|
||||
@@ -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-mt-12 tw-text-3xl tw-text-main">
|
||||
<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>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams-content",
|
||||
templateUrl: "teams-content.component.html",
|
||||
})
|
||||
export class TeamsContentComponent {}
|
||||
@@ -0,0 +1,94 @@
|
||||
<div *ngIf="accountCreateOnly" class="">
|
||||
<h1 class="tw-mt-12 tw-text-center tw-text-xl" *ngIf="!layout">{{ "createAccount" | i18n }}</h1>
|
||||
<div
|
||||
class="tw-min-w-xl tw-m-auto tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
|
||||
>
|
||||
<app-register-form
|
||||
[queryParamEmail]="email"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
></app-register-form>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!accountCreateOnly">
|
||||
<div
|
||||
class="tw-absolute tw--z-10 tw--mt-48 tw-h-96 tw-w-full tw--skew-y-3 tw-bg-background-alt2"
|
||||
></div>
|
||||
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl 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-border-secondary-300 tw-bg-background">
|
||||
<div class="tw-flex tw-h-12 tw-w-full tw-items-center tw-rounded-t tw-bg-secondary-100">
|
||||
<h2 class="tw-mb-0 tw-pl-4 tw-text-base tw-font-bold tw-uppercase">
|
||||
Start your 7-Day free trial of Bitwarden for {{ org }}
|
||||
</h2>
|
||||
</div>
|
||||
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
|
||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||
<app-register-form
|
||||
[isInTrialFlow]="true"
|
||||
(createdAccount)="createdAccount($event)"
|
||||
></app-register-form>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="orgInfoFormGroup.get('name').hasError('required')"
|
||||
cdkStepperNext
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
|
||||
<app-billing
|
||||
*ngIf="stepper.selectedIndex === 2"
|
||||
[plan]="plan"
|
||||
[product]="product"
|
||||
[orgInfoForm]="orgInfoFormGroup"
|
||||
(previousStep)="previousStep()"
|
||||
(onTrialBillingSuccess)="billingSuccess($event)"
|
||||
></app-billing>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
||||
<app-trial-confirmation-details
|
||||
[email]="email"
|
||||
[orgLabel]="orgLabel"
|
||||
></app-trial-confirmation-details>
|
||||
<div class="tw-mb-3 tw-flex">
|
||||
<button bitButton buttonType="primary" (click)="navigateToOrgVault()">
|
||||
Get Started
|
||||
</button>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="navigateToOrgInvite()"
|
||||
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
||||
>
|
||||
Invite Users
|
||||
</button>
|
||||
</div>
|
||||
</app-vertical-step>
|
||||
</app-vertical-stepper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,284 @@
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { FormBuilder, UntypedFormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Substitute } from "@fluffy-spoon/substitute";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/pipes/i18n.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService as BaseStateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PlanType } from "@bitwarden/common/enums/planType";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions";
|
||||
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
|
||||
describe("TrialInitiationComponent", () => {
|
||||
let component: TrialInitiationComponent;
|
||||
let fixture: ComponentFixture<TrialInitiationComponent>;
|
||||
const mockQueryParams = new BehaviorSubject<any>({ org: "enterprise" });
|
||||
const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974";
|
||||
const formBuilder: FormBuilder = new FormBuilder();
|
||||
let routerSpy: jest.SpyInstance;
|
||||
|
||||
let stateServiceMock: any;
|
||||
let apiServiceMock: any;
|
||||
let policyServiceMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// only mock functions we use in this component
|
||||
stateServiceMock = {
|
||||
getOrganizationInvitation: jest.fn(),
|
||||
};
|
||||
|
||||
apiServiceMock = {
|
||||
getPoliciesByToken: jest.fn(),
|
||||
};
|
||||
|
||||
policyServiceMock = {
|
||||
getMasterPasswordPolicyOptions: jest.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: "trial", component: TrialInitiationComponent },
|
||||
{
|
||||
path: `organizations/${testOrgId}/vault`,
|
||||
component: BlankComponent,
|
||||
},
|
||||
{
|
||||
path: `organizations/${testOrgId}/manage/people`,
|
||||
component: BlankComponent,
|
||||
},
|
||||
]),
|
||||
],
|
||||
declarations: [TrialInitiationComponent, I18nPipe],
|
||||
providers: [
|
||||
UntypedFormBuilder,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParams: mockQueryParams.asObservable(),
|
||||
},
|
||||
},
|
||||
{ provide: BaseStateService, useValue: stateServiceMock },
|
||||
{ provide: PolicyService, useValue: policyServiceMock },
|
||||
{ provide: ApiService, useValue: apiServiceMock },
|
||||
{ provide: LogService, useClass: Substitute.for<LogService>() },
|
||||
{ provide: I18nService, useClass: Substitute.for<I18nService>() },
|
||||
{ provide: TitleCasePipe, useClass: Substitute.for<TitleCasePipe>() },
|
||||
{
|
||||
provide: VerticalStepperComponent,
|
||||
useClass: VerticalStepperStubComponent,
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
// These tests demonstrate mocking service calls
|
||||
describe("onInit() enforcedPolicyOptions", () => {
|
||||
it("should not set enforcedPolicyOptions if state service returns no invite", async () => {
|
||||
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce(null);
|
||||
// Need to recreate component with new service mock
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.enforcedPolicyOptions).toBe(undefined);
|
||||
});
|
||||
it("should set enforcedPolicyOptions if state service returns an invite", async () => {
|
||||
// Set up service method mocks
|
||||
stateServiceMock.getOrganizationInvitation.mockReturnValueOnce({
|
||||
organizationId: testOrgId,
|
||||
token: "token",
|
||||
email: "testEmail",
|
||||
organizationUserId: "123",
|
||||
});
|
||||
apiServiceMock.getPoliciesByToken.mockReturnValueOnce({
|
||||
data: [
|
||||
{
|
||||
id: "345",
|
||||
organizationId: testOrgId,
|
||||
type: 1,
|
||||
data: [
|
||||
{
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
policyServiceMock.getMasterPasswordPolicyOptions.mockReturnValueOnce({
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
} as MasterPasswordPolicyOptions);
|
||||
|
||||
// Need to recreate component with new service mocks
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
expect(component.enforcedPolicyOptions).toMatchObject({
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// These tests demonstrate route params
|
||||
describe("Route params", () => {
|
||||
it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => {
|
||||
mockQueryParams.next({ org: "enterprise" });
|
||||
tick(); // wait for resolution
|
||||
fixture.detectChanges();
|
||||
component.ngOnInit();
|
||||
expect(component.org).toBe("enterprise");
|
||||
expect(component.plan).toBe(PlanType.EnterpriseAnnually);
|
||||
}));
|
||||
it("should not set org variable if no org param is provided", fakeAsync(() => {
|
||||
mockQueryParams.next({});
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.org).toBe("");
|
||||
expect(component.accountCreateOnly).toBe(true);
|
||||
}));
|
||||
it("should set the org to be families and plan to FamiliesAnnually if org param is invalid ", fakeAsync(async () => {
|
||||
mockQueryParams.next({ org: "hahahaha" });
|
||||
tick(); // wait for resolution
|
||||
fixture.detectChanges();
|
||||
component.ngOnInit();
|
||||
expect(component.org).toBe("families");
|
||||
expect(component.plan).toBe(PlanType.FamiliesAnnually);
|
||||
}));
|
||||
});
|
||||
|
||||
// These tests demonstrate the use of a stub component
|
||||
describe("createAccount()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should set email and call verticalStepper.next()", fakeAsync(() => {
|
||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
||||
component.createdAccount("test@email.com");
|
||||
expect(verticalStepperNext).toHaveBeenCalled();
|
||||
expect(component.email).toBe("test@email.com");
|
||||
}));
|
||||
});
|
||||
|
||||
describe("billingSuccess()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should set orgId and call verticalStepper.next()", () => {
|
||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
||||
component.billingSuccess({ orgId: testOrgId });
|
||||
expect(verticalStepperNext).toHaveBeenCalled();
|
||||
expect(component.orgId).toBe(testOrgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stepSelectionChange()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("on step 2 should show organization copy text", () => {
|
||||
component.stepSelectionChange({
|
||||
selectedIndex: 1,
|
||||
previouslySelectedIndex: 0,
|
||||
} as StepperSelectionEvent);
|
||||
|
||||
expect(component.orgInfoSubLabel).toContain("Enter your");
|
||||
expect(component.orgInfoSubLabel).toContain(" organization information");
|
||||
});
|
||||
it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => {
|
||||
component.orgInfoFormGroup = formBuilder.group({
|
||||
name: ["Hooli"],
|
||||
email: [""],
|
||||
});
|
||||
component.stepSelectionChange({
|
||||
selectedIndex: 2,
|
||||
previouslySelectedIndex: 1,
|
||||
} as StepperSelectionEvent);
|
||||
|
||||
expect(component.orgInfoSubLabel).toContain("Hooli");
|
||||
});
|
||||
});
|
||||
|
||||
describe("previousStep()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous");
|
||||
component.previousStep();
|
||||
expect(verticalStepperPrevious).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
// These tests demonstrate router navigation
|
||||
describe("navigation methods", () => {
|
||||
beforeEach(() => {
|
||||
component.orgId = testOrgId;
|
||||
const router = TestBed.inject(Router);
|
||||
fixture.detectChanges();
|
||||
routerSpy = jest.spyOn(router, "navigate");
|
||||
});
|
||||
describe("navigateToOrgVault", () => {
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
component.navigateToOrgVault();
|
||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]);
|
||||
}));
|
||||
});
|
||||
describe("navigateToOrgVault", () => {
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
component.navigateToOrgInvite();
|
||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "manage", "people"]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export class VerticalStepperStubComponent extends VerticalStepperComponent {}
|
||||
export class BlankComponent {} // For router tests
|
||||
@@ -0,0 +1,151 @@
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PlanType } from "@bitwarden/common/enums/planType";
|
||||
import { ProductType } from "@bitwarden/common/enums/productType";
|
||||
import { PolicyData } from "@bitwarden/common/models/data/policyData";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions";
|
||||
import { Policy } from "@bitwarden/common/models/domain/policy";
|
||||
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-trial",
|
||||
templateUrl: "trial-initiation.component.html",
|
||||
})
|
||||
export class TrialInitiationComponent implements OnInit {
|
||||
email = "";
|
||||
org = "";
|
||||
orgInfoSubLabel = "";
|
||||
orgId = "";
|
||||
orgLabel = "";
|
||||
billingSubLabel = "";
|
||||
plan: PlanType;
|
||||
product: ProductType;
|
||||
accountCreateOnly = true;
|
||||
policies: Policy[];
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
validOrgs: string[] = ["teams", "enterprise", "families"];
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
orgInfoFormGroup = this.formBuilder.group({
|
||||
name: ["", [Validators.required]],
|
||||
email: [""],
|
||||
});
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private titleCasePipe: TitleCasePipe,
|
||||
private stateService: StateService,
|
||||
private logService: LogService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.route.queryParams.pipe(first()).subscribe((qParams) => {
|
||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
|
||||
if (!qParams.org) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.validOrgs.includes(qParams.org)) {
|
||||
this.org = qParams.org;
|
||||
} else {
|
||||
this.org = "families";
|
||||
}
|
||||
|
||||
this.orgLabel = this.titleCasePipe.transform(this.org);
|
||||
this.accountCreateOnly = false;
|
||||
|
||||
if (this.org === "families") {
|
||||
this.plan = PlanType.FamiliesAnnually;
|
||||
this.product = ProductType.Families;
|
||||
} else if (this.org === "teams") {
|
||||
this.plan = PlanType.TeamsAnnually;
|
||||
this.product = ProductType.Teams;
|
||||
} else if (this.org === "enterprise") {
|
||||
this.plan = PlanType.EnterpriseAnnually;
|
||||
this.product = ProductType.Enterprise;
|
||||
}
|
||||
});
|
||||
|
||||
const invite = await this.stateService.getOrganizationInvitation();
|
||||
if (invite != null) {
|
||||
try {
|
||||
const policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId
|
||||
);
|
||||
if (policies.data != null) {
|
||||
const policiesData = policies.data.map((p) => new PolicyData(p));
|
||||
this.policies = policiesData.map((p) => new Policy(p));
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.policies != null) {
|
||||
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(
|
||||
this.policies
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
stepSelectionChange(event: StepperSelectionEvent) {
|
||||
// Set org info sub label
|
||||
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
||||
this.orgInfoSubLabel =
|
||||
"Enter your " + this.titleCasePipe.transform(this.org) + " organization information";
|
||||
} else if (event.previouslySelectedIndex === 1) {
|
||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
||||
}
|
||||
|
||||
//set billing sub label
|
||||
if (event.selectedIndex === 2) {
|
||||
this.billingSubLabel = this.i18nService.t("billingTrialSubLabel");
|
||||
}
|
||||
}
|
||||
|
||||
createdAccount(email: string) {
|
||||
this.email = email;
|
||||
this.orgInfoFormGroup.get("email")?.setValue(email);
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
billingSuccess(event: any) {
|
||||
this.orgId = event?.orgId;
|
||||
this.billingSubLabel = event?.subLabelText;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
navigateToOrgVault() {
|
||||
this.router.navigate(["organizations", this.orgId, "vault"]);
|
||||
}
|
||||
|
||||
navigateToOrgInvite() {
|
||||
this.router.navigate(["organizations", this.orgId, "manage", "people"]);
|
||||
}
|
||||
|
||||
previousStep() {
|
||||
this.verticalStepper.previous();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CdkStepperModule } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { FormFieldModule } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationCreateModule } from "../../organizations/create/organization-create.module";
|
||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||
import { RegisterFormModule } from "../register-form/register-form.module";
|
||||
|
||||
import { BillingComponent } from "./billing.component";
|
||||
import { ConfirmationDetailsComponent } from "./confirmation-details.component";
|
||||
import { EnterpriseContentComponent } from "./enterprise-content.component";
|
||||
import { FamiliesContentComponent } from "./families-content.component";
|
||||
import { TeamsContentComponent } from "./teams-content.component";
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
CdkStepperModule,
|
||||
VerticalStepperModule,
|
||||
FormFieldModule,
|
||||
RegisterFormModule,
|
||||
OrganizationCreateModule,
|
||||
LooseComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
TrialInitiationComponent,
|
||||
EnterpriseContentComponent,
|
||||
FamiliesContentComponent,
|
||||
TeamsContentComponent,
|
||||
ConfirmationDetailsComponent,
|
||||
BillingComponent,
|
||||
],
|
||||
exports: [TrialInitiationComponent],
|
||||
providers: [TitleCasePipe],
|
||||
})
|
||||
export class TrialInitiationModule {}
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="tw-m-2.5 tw-h-16 tw-text-center">
|
||||
<button
|
||||
(click)="selectStep()"
|
||||
[disabled]="disabled"
|
||||
class="tw-flex tw-w-full tw-items-center tw-border-none tw-bg-transparent"
|
||||
[ngClass]="{
|
||||
'hover:tw-bg-secondary-100': !disabled && step.editable
|
||||
}"
|
||||
[attr.aria-expanded]="selected"
|
||||
>
|
||||
<span
|
||||
class="tw-mr-3.5 tw-w-9 tw-rounded-full tw-font-bold tw-leading-9"
|
||||
*ngIf="!step.completed"
|
||||
[ngClass]="{
|
||||
'tw-bg-primary-500 tw-text-contrast': selected,
|
||||
'tw-bg-secondary-300 tw-text-main': !selected && !disabled && step.editable,
|
||||
'tw-bg-transparent tw-text-muted': disabled
|
||||
}"
|
||||
>
|
||||
{{ stepNumber }}
|
||||
</span>
|
||||
<span
|
||||
class="tw-mr-3.5 tw-w-9 tw-rounded-full tw-bg-primary-500 tw-font-bold tw-leading-9 tw-text-contrast"
|
||||
*ngIf="step.completed"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check tw-p-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div
|
||||
class="tw-txt-main tw-mt-3.5 tw-h-12 tw-text-left tw-leading-snug"
|
||||
[ngClass]="{
|
||||
'tw-font-bold': selected
|
||||
}"
|
||||
>
|
||||
<p
|
||||
class="main-label text tw-mb-1 tw-text-main"
|
||||
[ngClass]="{
|
||||
'tw-mt-1': !step.subLabel
|
||||
}"
|
||||
>
|
||||
{{ step.label }}
|
||||
</p>
|
||||
<p class="sub-label small tw-text-muted">{{ step.subLabel }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<ng-template>
|
||||
<div
|
||||
class="tw-inline-block tw-w-11/12 tw-pl-7"
|
||||
[ngClass]="{ 'tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300': applyBorder }"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,12 @@
|
||||
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 = "";
|
||||
@Input() applyBorder = true;
|
||||
}
|
||||
@@ -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-inline-block tw-pl-7"
|
||||
*ngIf="selectedIndex === i"
|
||||
[ngTemplateOutlet]="selected ? selected.content : null"
|
||||
></div>
|
||||
<div
|
||||
class="tw-ml-8 tw-h-6 tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300"
|
||||
*ngIf="!isLast && !(selectedIndex === i)"
|
||||
></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
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 {}
|
||||
Reference in New Issue
Block a user