From 9d35a8895efa2a131455e58d7a279a5eab101157 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:05:27 -0400 Subject: [PATCH] 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 --- apps/browser/src/_locales/en/messages.json | 9 + apps/desktop/src/locales/en/messages.json | 9 + apps/web/src/locales/en/messages.json | 33 +++ .../registration-env-selector.component.html | 1 + .../registration-env-selector.component.ts | 110 +++++++-- ...lf-hosted-env-config-dialog.component.html | 107 +++++++++ ...self-hosted-env-config-dialog.component.ts | 164 +++++++++++++ .../registration-start.component.html | 4 + .../registration-start.component.ts | 8 + .../registration-start/registration-start.mdx | 68 +++++- .../registration-start.stories.ts | 215 +++++++++++++++--- 11 files changed, 666 insertions(+), 62 deletions(-) create mode 100644 libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html create mode 100644 libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 03890c4d27f..deb7410a716 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -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" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 9a0a2b6a2c1..82d57c205d4 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -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" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 875de3980a5..d7a21ad6d6a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -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" }, diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html index b4dad835eec..9785bf05ab5 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html @@ -8,6 +8,7 @@ [label]="regionConfig.domain" > diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts index f01873dd3e2..fe41f0a3ac7 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts @@ -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(); 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(); 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) { - return of(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(); diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html new file mode 100644 index 00000000000..92c2f9f2f7a --- /dev/null +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html @@ -0,0 +1,107 @@ + + + Self-hosted environment + + + {{ "baseUrl" | i18n }} + + {{ "selfHostedBaseUrlHint" | i18n }} + + + + + {{ "customEnvironment" | i18n }} + + + + + {{ "selfHostedCustomEnvHeader" | i18n }} + + + + {{ "webVaultUrl" | i18n }} + + + + + {{ "apiUrl" | i18n }} + + + + + {{ "identityUrl" | i18n }} + + + + + {{ "notificationsUrl" | i18n }} + + + + + {{ "iconsUrl" | i18n }} + + + + + + {{ "selfHostedEnvFormInvalid" | i18n }} + + + + + {{ "save" | i18n }} + + + + {{ "cancel" | i18n }} + + + + diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts new file mode 100644 index 00000000000..2bedb4b3583 --- /dev/null +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts @@ -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 { + const dialogRef = dialogService.open(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(); + + constructor( + private dialogRef: DialogRef, + 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(); + } +} diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.component.html b/libs/auth/src/angular/registration/registration-start/registration-start.component.html index 8f64232f9c8..8da2eb76b55 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.component.html +++ b/libs/auth/src/angular/registration/registration-start/registration-start.component.html @@ -1,5 +1,9 @@ + + {{ "emailAddress" | i18n }} +Note that the self hosted option is not present in the environment selector. -### Self Hosted Example +### US Region - + -### Query Param Example +### EU Region + + + +### 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. - + + +## 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 + + + +### EU Region + + + +### Self Hosted + +Note the fact that the terms of service and privacy policy checkbox is not present when the +environment is self hosted. + + + +## 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 + + + +### EU Region + + + +### Self Hosted + +Note the fact that the terms of service and privacy policy checkbox is not present when the +environment is self hosted. + + diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index 099f086b963..50d1f15182e 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -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, + useValue: { queryParams: of(options.queryParams || {}) }, }, ], }), applicationConfig({ - providers: [importProvidersFrom(PreloadedEnglishI18nModule)], + providers: [ + importProvidersFrom(PreloadedEnglishI18nModule), + { + provide: EnvironmentService, + useValue: { + environment$: of({ + getRegion: () => options.defaultRegion || Region.US, + } as Partial), + availableRegions: () => [ + { key: Region.US, domain: "bitwarden.com", urls: {} }, + { key: Region.EU, domain: "bitwarden.eu", urls: {} }, + ], + setEnvironment: (region: Region, urls?: Urls) => Promise.resolve({}), + } as Partial, + }, + { + provide: PlatformUtilsService, + useValue: { + isSelfHost: () => options.isSelfHost || false, + getClientType: () => options.clientType || ClientType.Web, + } as Partial, + }, + { + provide: ToastService, + useValue: { + showToast: (options: ToastOptions) => {}, + } as Partial, + }, + ], }), ]; }; type Story = StoryObj; -export const CloudExample: Story = { - render: (args) => ({ - props: args, - template: ` - - `, - }), - decorators: decorators({ isSelfHost: false, queryParams: {} }), -}; - -export const SelfHostExample: Story = { - render: (args) => ({ - props: args, - template: ` - - `, - }), - 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: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Web, + queryParams: {}, + defaultRegion: Region.EU, + }), +}; + +export const WebUSRegionQueryParamsExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Web, + defaultRegion: Region.US, queryParams: { email: "jaredWasHere@bitwarden.com", emailReadonly: "true" }, }), }; + +export const DesktopUSRegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Desktop, + defaultRegion: Region.US, + isSelfHost: false, + }), +}; + +export const DesktopEURegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Desktop, + defaultRegion: Region.EU, + isSelfHost: false, + }), +}; + +export const DesktopSelfHostExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Desktop, + isSelfHost: true, + defaultRegion: Region.SelfHosted, + }), +}; + +export const BrowserExtensionUSRegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Browser, + defaultRegion: Region.US, + isSelfHost: false, + }), +}; + +export const BrowserExtensionEURegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Browser, + defaultRegion: Region.EU, + isSelfHost: false, + }), +}; + +export const BrowserExtensionSelfHostExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Browser, + isSelfHost: true, + defaultRegion: Region.SelfHosted, + }), +};
+ {{ "selfHostedCustomEnvHeader" | i18n }} +