Auth/PM-8367 - Email Verification - Integrate Registration Self Hosted Env Selector + new Self Hosted Env Settings Dialog into Registration Start (#9361)
* PM-8367 - WIP - initial comp creation * PM-8367 - Majority of new registration self hosted env config dialog working * PM-8367 - RegistrationEnvSelectorComponent - add method handleSelfHostedEnvConfigDialogResult and add toast for happy path. * PM-8367 - Add validation TODO * PM-8367 - RegistrationSelfHostedEnvConfigDialogComponent - Add validator * PM-8367 - RegEnvSelector - Only show self hosted if the client is browser or desktop since we will be using the selector on web as well. * PM-8367 - Registration start comp - add env selector * PM-8367 - Registration start - add proper import for standalone comps. * PM-8367 - Registration Start - get storybook fixed with registration env selector * PM-8367 - Add self hosted server to web translations only for storybook * PM-8367 - Add more storybook examples and update docs (WIP - need to test self hosted selection) * PM-8367 - Registration Start - update stories * PM-8367 - Env Selector now emits selected region so that parent comps can listen to it if needed. * PM-8367 - Registration Start - wire up handler for selectedRegionChange so that the parent comp can successfully track isSelfHost and hide / show the terms / privacy policy checkbox * PM-8367 - TODO cleanup * PM-8367 - Registration start docs - stage gate is two words. * PM-8367 - Per working session with Will, move top level provided services to app level instead of module level to solve dialog null injector errors. * PM-8367 - Storybook working for self hosted env dialog * PM-8367 - Add dialog scroll feature to bitDialog and implement in self hosted env dialog. * PM-8367 - Revert bit dialog changes and scroll implementation. * PM-8367 - Tweak registration start docs * PM-8367 - Remove unused changeDetectorRef * PM-8367 - Add docs per PR feedback
This commit is contained in:
parent
f691854387
commit
9d35a8895e
|
@ -1110,6 +1110,15 @@
|
|||
"selfHostedEnvironmentFooter": {
|
||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
|
||||
},
|
||||
"selfHostedBaseUrlHint": {
|
||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
|
||||
},
|
||||
"selfHostedCustomEnvHeader" :{
|
||||
"message": "For advanced configuration, you can specify the base URL of each service independently."
|
||||
},
|
||||
"selfHostedEnvFormInvalid" :{
|
||||
"message": "You must add either the base Server URL or at least one custom environment."
|
||||
},
|
||||
"customEnvironment": {
|
||||
"message": "Custom environment"
|
||||
},
|
||||
|
|
|
@ -695,6 +695,15 @@
|
|||
"selfHostedEnvironmentFooter": {
|
||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
|
||||
},
|
||||
"selfHostedBaseUrlHint": {
|
||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
|
||||
},
|
||||
"selfHostedCustomEnvHeader" :{
|
||||
"message": "For advanced configuration, you can specify the base URL of each service independently."
|
||||
},
|
||||
"selfHostedEnvFormInvalid" :{
|
||||
"message": "You must add either the base Server URL or at least one custom environment."
|
||||
},
|
||||
"customEnvironment": {
|
||||
"message": "Custom environment"
|
||||
},
|
||||
|
|
|
@ -5595,6 +5595,39 @@
|
|||
"rotateBillingSyncTokenTitle": {
|
||||
"message": "Rotating the billing sync token will invalidate the previous token."
|
||||
},
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
"customEnvironment": {
|
||||
"message": "Custom environment"
|
||||
},
|
||||
"selfHostedBaseUrlHint": {
|
||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
|
||||
},
|
||||
"selfHostedCustomEnvHeader" :{
|
||||
"message": "For advanced configuration, you can specify the base URL of each service independently."
|
||||
},
|
||||
"selfHostedEnvFormInvalid" :{
|
||||
"message": "You must add either the base Server URL or at least one custom environment."
|
||||
},
|
||||
"apiUrl": {
|
||||
"message": "API server URL"
|
||||
},
|
||||
"webVaultUrl": {
|
||||
"message": "Web vault server URL"
|
||||
},
|
||||
"identityUrl": {
|
||||
"message": "Identity server URL"
|
||||
},
|
||||
"notificationsUrl": {
|
||||
"message": "Notifications server URL"
|
||||
},
|
||||
"iconsUrl": {
|
||||
"message": "Icons server URL"
|
||||
},
|
||||
"environmentSaved": {
|
||||
"message": "Environment URLs saved"
|
||||
},
|
||||
"selfHostingTitle": {
|
||||
"message": "Self-hosting"
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
[label]="regionConfig.domain"
|
||||
></bit-option>
|
||||
<bit-option
|
||||
*ngIf="isDesktopOrBrowserExtension"
|
||||
[value]="ServerEnvironmentType.SelfHosted"
|
||||
[label]="'selfHostedServer' | i18n"
|
||||
></bit-option>
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { EMPTY, Subject, from, map, of, switchMap, takeUntil, tap } from "rxjs";
|
||||
import { Subject, from, map, of, pairwise, startWith, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
Region,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FormFieldModule, SelectModule } from "@bitwarden/components";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, FormFieldModule, SelectModule, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { RegistrationSelfHostedEnvConfigDialogComponent } from "./registration-self-hosted-env-config-dialog.component";
|
||||
|
||||
/**
|
||||
* Component for selecting the environment to register with in the email verification registration flow.
|
||||
* Outputs the selected region to the parent component so it can respond as necessary.
|
||||
*/
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-registration-env-selector",
|
||||
|
@ -19,7 +28,7 @@ import { FormFieldModule, SelectModule } from "@bitwarden/components";
|
|||
imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, SelectModule],
|
||||
})
|
||||
export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
||||
@Output() onOpenSelfHostedSettings = new EventEmitter();
|
||||
@Output() selectedRegionChange = new EventEmitter<RegionConfig | Region.SelfHosted | null>();
|
||||
|
||||
ServerEnvironmentType = Region;
|
||||
|
||||
|
@ -33,12 +42,24 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
|||
|
||||
availableRegionConfigs: RegionConfig[] = this.environmentService.availableRegions();
|
||||
|
||||
private selectedRegionFromEnv: RegionConfig | Region.SelfHosted;
|
||||
|
||||
isDesktopOrBrowserExtension = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
const clientType = platformUtilsService.getClientType();
|
||||
this.isDesktopOrBrowserExtension =
|
||||
clientType === ClientType.Desktop || clientType === ClientType.Browser;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.initSelectedRegionAndListenForEnvChanges();
|
||||
|
@ -61,13 +82,17 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
|||
|
||||
return regionConfig;
|
||||
}),
|
||||
tap((selectedRegionInitialValue: RegionConfig | Region.SelfHosted) => {
|
||||
// This inits the form control with the selected region, but
|
||||
// it also sets the value to self hosted if the self hosted settings are saved successfully
|
||||
// in the client specific implementation managed by the parent component.
|
||||
// It also resets the value to the previously selected region if the self hosted
|
||||
// settings are closed without saving. We don't emit the event to avoid a loop.
|
||||
this.selectedRegion.setValue(selectedRegionInitialValue, { emitEvent: false });
|
||||
tap((selectedRegionFromEnv: RegionConfig | Region.SelfHosted) => {
|
||||
// Only set the value if it is different from the current value.
|
||||
if (selectedRegionFromEnv !== this.selectedRegion.value) {
|
||||
// Don't emit to avoid triggering the selectedRegion valueChanges subscription
|
||||
// which could loop back to this code.
|
||||
this.selectedRegion.setValue(selectedRegionFromEnv, { emitEvent: false });
|
||||
}
|
||||
|
||||
// Save this off so we can reset the value to the previously selected region
|
||||
// if the self hosted settings are closed without saving.
|
||||
this.selectedRegionFromEnv = selectedRegionFromEnv;
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
|
@ -77,23 +102,66 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
|||
private listenForSelectedRegionChanges() {
|
||||
this.selectedRegion.valueChanges
|
||||
.pipe(
|
||||
switchMap((selectedRegionConfig: RegionConfig | Region.SelfHosted | null) => {
|
||||
if (selectedRegionConfig === null) {
|
||||
startWith(null), // required so that first user choice is not ignored
|
||||
pairwise(),
|
||||
switchMap(
|
||||
([prevSelectedRegion, selectedRegion]: [
|
||||
RegionConfig | Region.SelfHosted | null,
|
||||
RegionConfig | Region.SelfHosted | null,
|
||||
]) => {
|
||||
if (selectedRegion === null) {
|
||||
this.selectedRegionChange.emit(selectedRegion);
|
||||
return of(null);
|
||||
}
|
||||
|
||||
if (selectedRegionConfig === Region.SelfHosted) {
|
||||
this.onOpenSelfHostedSettings.emit();
|
||||
return EMPTY;
|
||||
if (selectedRegion === Region.SelfHosted) {
|
||||
return from(
|
||||
RegistrationSelfHostedEnvConfigDialogComponent.open(this.dialogService),
|
||||
).pipe(
|
||||
tap((result: boolean | undefined) =>
|
||||
this.handleSelfHostedEnvConfigDialogResult(result, prevSelectedRegion),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return from(this.environmentService.setEnvironment(selectedRegionConfig.key));
|
||||
}),
|
||||
this.selectedRegionChange.emit(selectedRegion);
|
||||
return from(this.environmentService.setEnvironment(selectedRegion.key));
|
||||
},
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private handleSelfHostedEnvConfigDialogResult(
|
||||
result: boolean | undefined,
|
||||
prevSelectedRegion: RegionConfig | Region.SelfHosted | null,
|
||||
) {
|
||||
if (result === true) {
|
||||
this.selectedRegionChange.emit(Region.SelfHosted);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the value to the previously selected region or the current env setting
|
||||
// if the self hosted env settings dialog is closed without saving.
|
||||
if (
|
||||
(result === false || result === undefined) &&
|
||||
prevSelectedRegion !== null &&
|
||||
prevSelectedRegion !== Region.SelfHosted
|
||||
) {
|
||||
this.selectedRegionChange.emit(prevSelectedRegion);
|
||||
this.selectedRegion.setValue(prevSelectedRegion, { emitEvent: false });
|
||||
} else {
|
||||
this.selectedRegionChange.emit(this.selectedRegionFromEnv);
|
||||
this.selectedRegion.setValue(this.selectedRegionFromEnv, { emitEvent: false });
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle> Self-hosted environment</span>
|
||||
<ng-container bitDialogContent>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "baseUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
id="self_hosted_env_settings_form_input_base_url"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="baseUrl"
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
/>
|
||||
<bit-hint>{{ "selfHostedBaseUrlHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<button bitLink linkType="primary" type="button" (click)="showCustomEnv = !showCustomEnv">
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm"
|
||||
[ngClass]="{ 'bwi-angle-right': !showCustomEnv, 'bwi-angle-down': showCustomEnv }"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{{ "customEnvironment" | i18n }}
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="showCustomEnv">
|
||||
<p bitTypography="body1" class="tw-text-muted tw-mt-3">
|
||||
{{ "selfHostedCustomEnvHeader" | i18n }}
|
||||
</p>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "webVaultUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
id="self_hosted_env_settings_form_input_web_vault_url"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="webVaultUrl"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "apiUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
id="self_hosted_env_settings_form_input_api_url"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="apiUrl"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "identityUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
id="self_hosted_env_settings_form_input_identity_url"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="identityUrl"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "notificationsUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
id="self_hosted_env_settings_form_input_notifications_url"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="notificationsUrl"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "iconsUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
id="self_hosted_env_settings_form_input_icons_url"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="iconsUrl"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
|
||||
<span
|
||||
*ngIf="showErrorSummary"
|
||||
class="tw-block tw-text-danger tw-mt-2"
|
||||
aria-live="assertive"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bwi bwi-error"></i> {{ "selfHostedEnvFormInvalid" | i18n }}
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
|
@ -0,0 +1,164 @@
|
|||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
} from "@angular/forms";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
/**
|
||||
* Validator for self-hosted environment settings form.
|
||||
* It enforces that at least one URL is provided.
|
||||
*/
|
||||
function selfHostedEnvSettingsFormValidator(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const formGroup = control as FormGroup;
|
||||
const baseUrl = formGroup.get("baseUrl")?.value;
|
||||
const webVaultUrl = formGroup.get("webVaultUrl")?.value;
|
||||
const apiUrl = formGroup.get("apiUrl")?.value;
|
||||
const identityUrl = formGroup.get("identityUrl")?.value;
|
||||
const iconsUrl = formGroup.get("iconsUrl")?.value;
|
||||
const notificationsUrl = formGroup.get("notificationsUrl")?.value;
|
||||
|
||||
if (baseUrl || webVaultUrl || apiUrl || identityUrl || iconsUrl || notificationsUrl) {
|
||||
return null; // valid
|
||||
} else {
|
||||
return { atLeastOneUrlIsRequired: true }; // invalid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog for configuring self-hosted environment settings.
|
||||
*/
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-registration-self-hosted-env-config-dialog",
|
||||
templateUrl: "registration-self-hosted-env-config-dialog.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
DialogModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
],
|
||||
})
|
||||
export class RegistrationSelfHostedEnvConfigDialogComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Opens the dialog.
|
||||
* @param dialogService - Dialog service.
|
||||
* @returns Promise that resolves to true if the dialog was closed with a successful result, false otherwise.
|
||||
*/
|
||||
static async open(dialogService: DialogService): Promise<boolean> {
|
||||
const dialogRef = dialogService.open<boolean>(RegistrationSelfHostedEnvConfigDialogComponent, {
|
||||
disableClose: false,
|
||||
});
|
||||
|
||||
const dialogResult = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
return dialogResult;
|
||||
}
|
||||
|
||||
formGroup = this.formBuilder.group(
|
||||
{
|
||||
baseUrl: [null],
|
||||
webVaultUrl: [null],
|
||||
apiUrl: [null],
|
||||
identityUrl: [null],
|
||||
iconsUrl: [null],
|
||||
notificationsUrl: [null],
|
||||
},
|
||||
{ validators: selfHostedEnvSettingsFormValidator() },
|
||||
);
|
||||
|
||||
get baseUrl(): FormControl {
|
||||
return this.formGroup.get("baseUrl") as FormControl;
|
||||
}
|
||||
|
||||
get webVaultUrl(): FormControl {
|
||||
return this.formGroup.get("webVaultUrl") as FormControl;
|
||||
}
|
||||
|
||||
get apiUrl(): FormControl {
|
||||
return this.formGroup.get("apiUrl") as FormControl;
|
||||
}
|
||||
|
||||
get identityUrl(): FormControl {
|
||||
return this.formGroup.get("identityUrl") as FormControl;
|
||||
}
|
||||
|
||||
get iconsUrl(): FormControl {
|
||||
return this.formGroup.get("iconsUrl") as FormControl;
|
||||
}
|
||||
|
||||
get notificationsUrl(): FormControl {
|
||||
return this.formGroup.get("notificationsUrl") as FormControl;
|
||||
}
|
||||
|
||||
showCustomEnv = false;
|
||||
showErrorSummary = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef<boolean>,
|
||||
private formBuilder: FormBuilder,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
submit = async () => {
|
||||
this.showErrorSummary = false;
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
this.showErrorSummary = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.environmentService.setEnvironment(Region.SelfHosted, {
|
||||
base: this.baseUrl.value,
|
||||
api: this.apiUrl.value,
|
||||
identity: this.identityUrl.value,
|
||||
webVault: this.webVaultUrl.value,
|
||||
icons: this.iconsUrl.value,
|
||||
notifications: this.notificationsUrl.value,
|
||||
});
|
||||
|
||||
this.dialogRef.close(true);
|
||||
};
|
||||
|
||||
async cancel() {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
<ng-container *ngIf="state === RegistrationStartState.USER_DATA_ENTRY">
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<auth-registration-env-selector
|
||||
(selectedRegionChange)="handleSelectedRegionChange($event)"
|
||||
></auth-registration-env-selector>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { RegionConfig, Region } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
|
@ -23,6 +24,7 @@ import {
|
|||
} from "@bitwarden/components";
|
||||
|
||||
import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon";
|
||||
import { RegistrationEnvSelectorComponent } from "../registration-env-selector/registration-env-selector.component";
|
||||
|
||||
export enum RegistrationStartState {
|
||||
USER_DATA_ENTRY = "UserDataEntry",
|
||||
|
@ -43,6 +45,7 @@ export enum RegistrationStartState {
|
|||
ButtonModule,
|
||||
LinkModule,
|
||||
IconModule,
|
||||
RegistrationEnvSelectorComponent,
|
||||
],
|
||||
})
|
||||
export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
|
@ -84,6 +87,7 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
|||
private route: ActivatedRoute,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
// TODO: this needs to update if user selects self hosted
|
||||
this.isSelfHost = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
|
@ -116,6 +120,10 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
|||
this.registrationStartStateChange.emit(this.state);
|
||||
};
|
||||
|
||||
handleSelectedRegionChange(region: RegionConfig | Region.SelfHosted | null) {
|
||||
this.isSelfHost = region === Region.SelfHosted;
|
||||
}
|
||||
|
||||
private validateForm(): boolean {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
|
|
|
@ -7,22 +7,72 @@ import * as stories from "./registration-start.stories";
|
|||
# RegistrationStart Component
|
||||
|
||||
The Auth-owned RegistrationStartComponent is to be used for the first step in the new email
|
||||
verification stagegated registration process. It collects the user's email address (required) and
|
||||
optionally their name. On cloud environments, it requires acceptance of the terms of service and the
|
||||
privacy policy; the checkbox is hidden on self hosted environments.
|
||||
verification stage gated registration process. It collects the environment (required), the user's
|
||||
email address (required) and optionally their name. On cloud environments, it requires acceptance of
|
||||
the terms of service and the privacy policy; the checkbox is hidden on self hosted environments.
|
||||
|
||||
### Cloud Example
|
||||
## Web Examples
|
||||
|
||||
<Story of={stories.CloudExample} />
|
||||
Note that the self hosted option is not present in the environment selector.
|
||||
|
||||
### Self Hosted Example
|
||||
### US Region
|
||||
|
||||
<Story of={stories.SelfHostExample} />
|
||||
<Story of={stories.WebUSRegionExample} />
|
||||
|
||||
### Query Param Example
|
||||
### EU Region
|
||||
|
||||
<Story of={stories.WebEURegionExample} />
|
||||
|
||||
### Query Params
|
||||
|
||||
The component accepts two query parameters: `email` and `emailReadonly`. If an email is provided, it
|
||||
will be pre-filled in the email input field. If `emailReadonly` is set to `true`, the email input
|
||||
field will be set to readonly. `emailReadonly` is primarily for the organization invite flow.
|
||||
|
||||
<Story of={stories.QueryParamsExample} />
|
||||
<Story of={stories.WebUSRegionQueryParamsExample} />
|
||||
|
||||
## Desktop
|
||||
|
||||
Behavior to note:
|
||||
|
||||
- The self hosted option is present in the environment selector.
|
||||
- If you go from non-self hosted to self hosted, the terms of service and privacy policy checkbox
|
||||
will disappear.
|
||||
|
||||
### US Region
|
||||
|
||||
<Story of={stories.DesktopUSRegionExample} />
|
||||
|
||||
### EU Region
|
||||
|
||||
<Story of={stories.DesktopEURegionExample} />
|
||||
|
||||
### Self Hosted
|
||||
|
||||
Note the fact that the terms of service and privacy policy checkbox is not present when the
|
||||
environment is self hosted.
|
||||
|
||||
<Story of={stories.DesktopSelfHostExample} />
|
||||
|
||||
## Browser Extension
|
||||
|
||||
Behavior to note:
|
||||
|
||||
- The self hosted option is present in the environment selector.
|
||||
- If you go from non-self hosted to self hosted, the terms of service and privacy policy checkbox
|
||||
will disappear.
|
||||
|
||||
### US Region
|
||||
|
||||
<Story of={stories.BrowserExtensionUSRegionExample} />
|
||||
|
||||
### EU Region
|
||||
|
||||
<Story of={stories.BrowserExtensionEURegionExample} />
|
||||
|
||||
### Self Hosted
|
||||
|
||||
Note the fact that the terms of service and privacy policy checkbox is not present when the
|
||||
environment is self hosted.
|
||||
|
||||
<Story of={stories.BrowserExtensionSelfHostExample} />
|
||||
|
|
|
@ -1,10 +1,30 @@
|
|||
import { importProvidersFrom } from "@angular/core";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ActivatedRoute, Params } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
Region,
|
||||
Urls,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
LinkModule,
|
||||
SelectModule,
|
||||
ToastOptions,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests";
|
||||
|
||||
|
@ -15,52 +35,70 @@ export default {
|
|||
component: RegistrationStartComponent,
|
||||
} as Meta;
|
||||
|
||||
const decorators = (options: { isSelfHost: boolean; queryParams: Params }) => {
|
||||
const decorators = (options: {
|
||||
isSelfHost?: boolean;
|
||||
queryParams?: Params;
|
||||
clientType?: ClientType;
|
||||
defaultRegion?: Region;
|
||||
}) => {
|
||||
return [
|
||||
moduleMetadata({
|
||||
imports: [RouterTestingModule],
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
DialogModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
SelectModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
AsyncActionsModule,
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { queryParams: of(options.queryParams) },
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: {
|
||||
isSelfHost: () => options.isSelfHost,
|
||||
} as Partial<PlatformUtilsService>,
|
||||
useValue: { queryParams: of(options.queryParams || {}) },
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
providers: [
|
||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
environment$: of({
|
||||
getRegion: () => options.defaultRegion || Region.US,
|
||||
} as Partial<Environment>),
|
||||
availableRegions: () => [
|
||||
{ key: Region.US, domain: "bitwarden.com", urls: {} },
|
||||
{ key: Region.EU, domain: "bitwarden.eu", urls: {} },
|
||||
],
|
||||
setEnvironment: (region: Region, urls?: Urls) => Promise.resolve({}),
|
||||
} as Partial<EnvironmentService>,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: {
|
||||
isSelfHost: () => options.isSelfHost || false,
|
||||
getClientType: () => options.clientType || ClientType.Web,
|
||||
} as Partial<PlatformUtilsService>,
|
||||
},
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: {
|
||||
showToast: (options: ToastOptions) => {},
|
||||
} as Partial<ToastService>,
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
type Story = StoryObj<RegistrationStartComponent>;
|
||||
|
||||
export const CloudExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({ isSelfHost: false, queryParams: {} }),
|
||||
};
|
||||
|
||||
export const SelfHostExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({ isSelfHost: true, queryParams: {} }),
|
||||
};
|
||||
|
||||
export const QueryParamsExample: Story = {
|
||||
export const WebUSRegionExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
|
@ -68,7 +106,120 @@ export const QueryParamsExample: Story = {
|
|||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
isSelfHost: false,
|
||||
clientType: ClientType.Web,
|
||||
queryParams: {},
|
||||
defaultRegion: Region.US,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WebEURegionExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Web,
|
||||
queryParams: {},
|
||||
defaultRegion: Region.EU,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WebUSRegionQueryParamsExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Web,
|
||||
defaultRegion: Region.US,
|
||||
queryParams: { email: "jaredWasHere@bitwarden.com", emailReadonly: "true" },
|
||||
}),
|
||||
};
|
||||
|
||||
export const DesktopUSRegionExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Desktop,
|
||||
defaultRegion: Region.US,
|
||||
isSelfHost: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export const DesktopEURegionExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Desktop,
|
||||
defaultRegion: Region.EU,
|
||||
isSelfHost: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export const DesktopSelfHostExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Desktop,
|
||||
isSelfHost: true,
|
||||
defaultRegion: Region.SelfHosted,
|
||||
}),
|
||||
};
|
||||
|
||||
export const BrowserExtensionUSRegionExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Browser,
|
||||
defaultRegion: Region.US,
|
||||
isSelfHost: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export const BrowserExtensionEURegionExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Browser,
|
||||
defaultRegion: Region.EU,
|
||||
isSelfHost: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export const BrowserExtensionSelfHostExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Browser,
|
||||
isSelfHost: true,
|
||||
defaultRegion: Region.SelfHosted,
|
||||
}),
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue