1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

Auth/PM-7324 - Registration with Email Verification - Registration Start Component Implementation (#9573)

* PM-7324 - Register new registration start comp at signup route on web

* PM-7324 - Add registerSendVerificationEmail logic in API service layer.

* PM-7324 - Update registration start comp to actually send information to API and trigger email.

* PM-7324 - progress on opt in for marketing emails redesign.

* PM-7324 - Add feature flag and feature flag guard to sign up route.

* PM-7324 - RegistrationEnvSelector - emit initial value

* PM-7324 - Registration Start comp - wire up setReceiveMarketingEmailsByRegion logic.

* PM-7324 - Registration start html - use proper link for email pref management.

* PM-7324 - Translate text

* PM-7324 - Design pass

* PM-7324 - design pass v2

* PM-7324 - Update Tailwind config to add availability of anon layout to desktop and browser extension

* PM-7324 - Desktop - AppRoutingModule - Add new signup route protected by the email verification feature flag.

* PM-7324 - BrowserExtension - AppRoutingModule - Add signup route protected by feature flag

* PM-7324 - Feature flag all register page navigations to redirect users to the new signup page.

* PM-7324 - Update AnonLayoutWrapperComponent constructor logic to avoid passing undefined values into I18nService.t method

* PM-7324 - Accept org invite web comp - adjust register url and qParams

* PM-7324 - Add AnonLayoutWrapperData to desktop & browser since we don't need titleId.

* PM-7324 - Revert anon layout wrapper comp changes as they were made separately and merged to main.

* PM-7234 - Fix registration start component so the login route works for the browser extension.

* PM-7324 - Registration start story now building again + fix storybook warning around BrowserAnimationsModule

* PM-7324 - Registration Start - add missing tw-text-main to fix dark mode rendering.

* PM-7324 - Update storybook docs

* PM-7324 - Get stub of registration finish component so that the verify email has something to land on.

* PM-7324 - Registration start - receive marketing materials should never be required.

* PM-7324 - Add finish signup route + required translations to desktop & browser.

* PM-7324 - AnonLayoutWrapperComponent - Resolve issues where navigating to a sibling anonymous route wouldn't update the AnonLayoutWrapperData.

* PM-7324 - Remove unnecessary array

* PM-7324  - Per PR feedback, improve setReceiveMarketingEmailsByRegion

* PM-7324 - Per PR feedback, inject login routes via route data

* PM-7324 - Document methods in account api service

* PM-7324 - PR feedback - jsdoc tweaks
This commit is contained in:
Jared Snider
2024-06-14 11:40:56 -04:00
committed by GitHub
parent eb96f7dbfb
commit 215bbc2f8e
42 changed files with 584 additions and 88 deletions

View File

@@ -93,6 +93,9 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
// 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;
// Emit the initial value
this.selectedRegionChange.emit(selectedRegionFromEnv);
}),
takeUntil(this.destroy$),
)

View File

@@ -0,0 +1 @@
<h3>This component will be built in the next phase of email verification work.</h3>

View File

@@ -0,0 +1,15 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@Component({
standalone: true,
selector: "auth-registration-finish",
templateUrl: "./registration-finish.component.html",
imports: [CommonModule, JslibModule, RouterModule],
})
export class RegistrationFinishComponent {
constructor() {}
}

View File

@@ -1,3 +1,3 @@
<span
>{{ "alreadyHaveAccount" | i18n }} <a routerLink="/login">{{ "logIn" | i18n }}</a></span
>{{ "alreadyHaveAccount" | i18n }} <a [routerLink]="loginRoute">{{ "logIn" | i18n }}</a></span
>

View File

@@ -1,15 +1,32 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
/**
* RegistrationStartSecondaryComponentData
* @loginRoute: string - The client specific route to the login page - configured at the app-routing.module level.
*/
export interface RegistrationStartSecondaryComponentData {
loginRoute: string;
}
@Component({
standalone: true,
selector: "auth-registration-start-secondary",
templateUrl: "./registration-start-secondary.component.html",
imports: [CommonModule, JslibModule, RouterModule],
})
export class RegistrationStartSecondaryComponent {
constructor() {}
export class RegistrationStartSecondaryComponent implements OnInit {
loginRoute: string;
constructor(private activatedRoute: ActivatedRoute) {}
async ngOnInit() {
const routeData = await firstValueFrom(this.activatedRoute.data);
this.loginRoute = routeData["loginRoute"];
}
}

View File

@@ -23,38 +23,60 @@
<bit-form-control *ngIf="!isSelfHost">
<input
id="register-start-form-input-accept-policies"
id="register-start-form-input-receive-marketing-emails"
type="checkbox"
bitCheckbox
formControlName="acceptPolicies"
formControlName="receiveMarketingEmails"
/>
<bit-label for="register-start-form-input-accept-policies">
{{ "acceptPolicies" | i18n }}
<bit-label for="register-start-form-input-receive-marketing-emails">
{{ "receiveMarketingEmails" | i18n }}
<a
bitLink
linkType="primary"
href="https://bitwarden.com/terms/"
href="https://bitwarden.com/email-preferences"
target="_blank"
rel="noreferrer"
>{{ "termsOfService" | i18n }}</a
>,
<a
bitLink
linkType="primary"
href="https://bitwarden.com/privacy/"
target="_blank"
rel="noreferrer"
>{{ "privacyPolicy" | i18n }}</a
>{{ "unsubscribe" | i18n }}</a
>
{{ "atAnyTime" | i18n }}
</bit-label>
</bit-form-control>
<button [block]="true" type="submit" buttonType="primary" bitButton bitFormButton>
<button
[block]="true"
type="submit"
buttonType="primary"
bitButton
bitFormButton
class="tw-mb-3"
>
{{ "continue" | i18n }}
</button>
<p bitTypography="helper" class="tw-text-main tw-text-xs tw-mb-0">
{{ "byContinuingYouAgreeToThe" | i18n }}
<a
bitLink
linkType="primary"
href="https://bitwarden.com/terms/"
target="_blank"
rel="noreferrer"
>{{ "termsOfService" | i18n }}</a
>
{{ "and" | i18n }}
<a
bitLink
linkType="primary"
href="https://bitwarden.com/privacy/"
target="_blank"
rel="noreferrer"
>{{ "privacyPolicy" | i18n }}</a
>
</p>
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary></form
></ng-container>
<ng-container *ngIf="state === RegistrationStartState.CHECK_EMAIL">
<div class="tw-flex tw-flex-col tw-items-center tw-justify-center">
<bit-icon [icon]="Icons.RegistrationCheckEmailIcon" class="tw-mb-6"></bit-icon>

View File

@@ -1,17 +1,12 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
import {
AbstractControl,
FormBuilder,
FormControl,
ReactiveFormsModule,
ValidatorFn,
Validators,
} from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { RegisterSendVerificationEmailRequest } from "@bitwarden/common/auth/models/request/registration/register-send-verification-email.request";
import { RegionConfig, Region } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
@@ -31,6 +26,12 @@ export enum RegistrationStartState {
CHECK_EMAIL = "CheckEmail",
}
const DEFAULT_MARKETING_EMAILS_PREF_BY_REGION: Record<Region, boolean> = {
[Region.US]: true,
[Region.EU]: false,
[Region.SelfHosted]: false,
};
@Component({
standalone: true,
selector: "auth-registration-start",
@@ -60,20 +61,19 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]],
name: [""],
acceptPolicies: [false, [this.acceptPoliciesValidator()]],
selectedRegion: [null],
receiveMarketingEmails: [false],
});
get email(): FormControl {
return this.formGroup.get("email") as FormControl;
get email() {
return this.formGroup.controls.email;
}
get name(): FormControl {
return this.formGroup.get("name") as FormControl;
get name() {
return this.formGroup.controls.name;
}
get acceptPolicies(): FormControl {
return this.formGroup.get("acceptPolicies") as FormControl;
get receiveMarketingEmails() {
return this.formGroup.controls.receiveMarketingEmails;
}
emailReadonly: boolean = false;
@@ -86,8 +86,9 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private platformUtilsService: PlatformUtilsService,
private accountApiService: AccountApiService,
private router: Router,
) {
// TODO: this needs to update if user selects self hosted
this.isSelfHost = platformUtilsService.isSelfHost();
}
@@ -107,6 +108,18 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
});
}
setReceiveMarketingEmailsByRegion(region: RegionConfig | Region.SelfHosted) {
let defaultValue;
if (region === Region.SelfHosted) {
defaultValue = DEFAULT_MARKETING_EMAILS_PREF_BY_REGION[region];
} else {
const regionKey = (region as RegionConfig).key;
defaultValue = DEFAULT_MARKETING_EMAILS_PREF_BY_REGION[regionKey];
}
this.receiveMarketingEmails.setValue(defaultValue);
}
submit = async () => {
const valid = this.validateForm();
@@ -114,14 +127,31 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
return;
}
// TODO: Implement registration logic
const request: RegisterSendVerificationEmailRequest = new RegisterSendVerificationEmailRequest(
this.email.value,
this.name.value,
this.receiveMarketingEmails.value,
);
const result = await this.accountApiService.registerSendVerificationEmail(request);
if (typeof result === "string") {
// we received a token, so the env doesn't support email verification
// send the user directly to the finish registration page with the token as a query param
await this.router.navigate(["/finish-signup"], { queryParams: { token: result } });
}
// Result is null, so email verification is required
this.state = RegistrationStartState.CHECK_EMAIL;
this.registrationStartStateChange.emit(this.state);
};
handleSelectedRegionChange(region: RegionConfig | Region.SelfHosted | null) {
this.isSelfHost = region === Region.SelfHosted;
if (region !== null) {
this.setReceiveMarketingEmailsByRegion(region);
}
}
private validateForm(): boolean {
@@ -139,14 +169,6 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
this.registrationStartStateChange.emit(this.state);
}
private acceptPoliciesValidator(): ValidatorFn {
return (control: AbstractControl) => {
const ctrlValue = control.value;
return !ctrlValue && !this.isSelfHost ? { required: true } : null;
};
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();

View File

@@ -8,8 +8,9 @@ import * as stories from "./registration-start.stories";
The Auth-owned RegistrationStartComponent is to be used for the first step in the new email
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.
email address (required) and optionally their name. On cloud environments, it offers a checkbox for
the user to choose to receive marketing emails or not with the default value changing based on the
environment (e.g., true for US, false for EU).
## Web Examples
@@ -36,8 +37,10 @@ field will be set to readonly. `emailReadonly` is primarily for the organization
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.
- If you go from non-self hosted to self hosted, the receive marketing emails checkbox will
disappear.
- If you change regions, the receive marketing emails checkbox default value will change based on
the region.
### US Region
@@ -49,8 +52,8 @@ Behavior to note:
### Self Hosted
Note the fact that the terms of service and privacy policy checkbox is not present when the
environment is self hosted.
Note the fact that the receive marketing emails checkbox is not present when the environment is self
hosted.
<Story of={stories.DesktopSelfHostExample} />
@@ -59,8 +62,10 @@ environment is self hosted.
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.
- If you go from non-self hosted to self hosted, the receive marketing emails checkbox will
disappear.
- If you change regions, the receive marketing emails checkbox default value will change based on
the region.
### US Region
@@ -72,7 +77,7 @@ Behavior to note:
### Self Hosted
Note the fact that the terms of service and privacy policy checkbox is not present when the
environment is self hosted.
Note the fact that the receive marketing emails checkbox is not present when the environment is self
hosted.
<Story of={stories.BrowserExtensionSelfHostExample} />

View File

@@ -6,6 +6,7 @@ import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { of } from "rxjs";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { ClientType } from "@bitwarden/common/enums";
import {
Environment,
@@ -53,7 +54,6 @@ const decorators = (options: {
LinkModule,
TypographyModule,
AsyncActionsModule,
BrowserAnimationsModule,
],
providers: [
{
@@ -64,6 +64,7 @@ const decorators = (options: {
}),
applicationConfig({
providers: [
importProvidersFrom(BrowserAnimationsModule),
importProvidersFrom(PreloadedEnglishI18nModule),
{
provide: EnvironmentService,
@@ -91,6 +92,12 @@ const decorators = (options: {
showToast: (options: ToastOptions) => {},
} as Partial<ToastService>,
},
{
provide: AccountApiService,
useValue: {
registerSendVerificationEmail: () => Promise.resolve(null),
} as Partial<AccountApiService>,
},
],
}),
];