1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-03 04:59:06 +02: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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 584 additions and 88 deletions

View File

@ -16,6 +16,12 @@
"createAccount": { "createAccount": {
"message": "Create account" "message": "Create account"
}, },
"setAStrongPassword": {
"message": "Set a strong password"
},
"finishCreatingYourAccountBySettingAPassword": {
"message": "Finish creating your account by setting a password"
},
"login": { "login": {
"message": "Log in" "message": "Log in"
}, },
@ -1780,6 +1786,21 @@
"masterPasswordPolicyRequirementsNotMet": { "masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements." "message": "Your new master password does not meet the policy requirements."
}, },
"receiveMarketingEmails": {
"message": "Get emails from Bitwarden for announcements, advice, and research opportunities."
},
"unsubscribe": {
"message": "Unsubscribe"
},
"atAnyTime": {
"message": "at any time."
},
"byContinuingYouAgreeToThe": {
"message": "By continuing, you agree to the"
},
"and": {
"message": "and"
},
"acceptPolicies": { "acceptPolicies": {
"message": "By checking this box you agree to the following:" "message": "By checking this box you agree to the following:"
}, },

View File

@ -30,7 +30,9 @@
</form> </form>
<p class="createAccountLink"> <p class="createAccountLink">
{{ "newAroundHere" | i18n }} {{ "newAroundHere" | i18n }}
<a routerLink="/register" (click)="setLoginEmailValues()">{{ "createAccount" | i18n }}</a> <a [routerLink]="registerRoute" (click)="setLoginEmailValues()">{{
"createAccount" | i18n
}}</a>
</p> </p>
</div> </div>
</div> </div>

View File

@ -5,6 +5,8 @@ import { Subject, firstValueFrom, takeUntil } from "rxjs";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -26,6 +28,9 @@ export class HomeComponent implements OnInit, OnDestroy {
rememberEmail: [false], rememberEmail: [false],
}); });
// TODO: remove when email verification flag is removed
registerRoute = "/register";
constructor( constructor(
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
@ -34,9 +39,19 @@ export class HomeComponent implements OnInit, OnDestroy {
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private loginEmailService: LoginEmailServiceAbstraction, private loginEmailService: LoginEmailServiceAbstraction,
private accountSwitcherService: AccountSwitcherService, private accountSwitcherService: AccountSwitcherService,
private configService: ConfigService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
// TODO: remove when email verification flag is removed
const emailVerification = await this.configService.getFeatureFlag(
FeatureFlag.EmailVerification,
);
if (emailVerification) {
this.registerRoute = "/signup";
}
const email = this.loginEmailService.getEmail(); const email = this.loginEmailService.getEmail();
const rememberEmail = this.loginEmailService.getRememberEmail(); const rememberEmail = this.loginEmailService.getRememberEmail();

View File

@ -13,6 +13,7 @@ import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstraction
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -51,6 +52,7 @@ export class LoginComponent extends BaseLoginComponent {
loginEmailService: LoginEmailServiceAbstraction, loginEmailService: LoginEmailServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction,
configService: ConfigService,
) { ) {
super( super(
devicesApiService, devicesApiService,
@ -71,6 +73,7 @@ export class LoginComponent extends BaseLoginComponent {
loginEmailService, loginEmailService,
ssoLoginService, ssoLoginService,
webAuthnLoginService, webAuthnLoginService,
configService,
); );
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {
await syncService.fullSync(true); await syncService.fullSync(true);

View File

@ -8,6 +8,16 @@ import {
tdeDecryptionRequiredGuard, tdeDecryptionRequiredGuard,
unauthGuardFn, unauthGuardFn,
} from "@bitwarden/angular/auth/guards"; } from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
RegistrationFinishComponent,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
@ -343,6 +353,45 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { state: "update-temp-password" }, data: { state: "update-temp-password" },
}, },
{
path: "",
component: AnonLayoutWrapperComponent,
children: [
{
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: { pageTitle: "createAccount" } satisfies AnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationStartComponent,
},
{
path: "",
component: RegistrationStartSecondaryComponent,
outlet: "secondary",
data: {
loginRoute: "/home",
} satisfies RegistrationStartSecondaryComponentData,
},
],
},
{
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
} satisfies AnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationFinishComponent,
},
],
},
],
},
...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, { ...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, {
path: "about", path: "about",
canActivate: [AuthGuard], canActivate: [AuthGuard],

View File

@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base");
config.content = [ config.content = [
"./src/**/*.{html,ts}", "./src/**/*.{html,ts}",
"../../libs/components/src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}",
"../../libs/auth/src/**/*.{html,ts}",
"../../libs/angular/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}",
]; ];

View File

@ -6,7 +6,18 @@ import {
lockGuard, lockGuard,
redirectGuard, redirectGuard,
tdeDecryptionRequiredGuard, tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards"; } from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
RegistrationFinishComponent,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
@ -82,6 +93,45 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { titleId: "removeMasterPassword" }, data: { titleId: "removeMasterPassword" },
}, },
{
path: "",
component: AnonLayoutWrapperComponent,
children: [
{
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: { pageTitle: "createAccount" } satisfies AnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationStartComponent,
},
{
path: "",
component: RegistrationStartSecondaryComponent,
outlet: "secondary",
data: {
loginRoute: "/login",
} satisfies RegistrationStartSecondaryComponentData,
},
],
},
{
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
} satisfies AnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationFinishComponent,
},
],
},
],
},
]; ];
@NgModule({ @NgModule({

View File

@ -48,7 +48,7 @@
</div> </div>
<div class="sub-options"> <div class="sub-options">
<p class="no-margin">{{ "newAroundHere" | i18n }}</p> <p class="no-margin">{{ "newAroundHere" | i18n }}</p>
<button type="button" class="text text-primary" routerLink="/register"> <button type="button" class="text text-primary" [routerLink]="registerRoute">
{{ "createAccount" | i18n }} {{ "createAccount" | i18n }}
</button> </button>
</div> </div>

View File

@ -15,6 +15,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -71,6 +72,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
loginEmailService: LoginEmailServiceAbstraction, loginEmailService: LoginEmailServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction,
configService: ConfigService,
) { ) {
super( super(
devicesApiService, devicesApiService,
@ -91,6 +93,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
loginEmailService, loginEmailService,
ssoLoginService, ssoLoginService,
webAuthnLoginService, webAuthnLoginService,
configService,
); );
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = () => {
return syncService.fullSync(true); return syncService.fullSync(true);

View File

@ -499,6 +499,12 @@
"createAccount": { "createAccount": {
"message": "Create account" "message": "Create account"
}, },
"setAStrongPassword": {
"message": "Set a strong password"
},
"finishCreatingYourAccountBySettingAPassword": {
"message": "Finish creating your account by setting a password"
},
"logIn": { "logIn": {
"message": "Log in" "message": "Log in"
}, },
@ -1659,6 +1665,21 @@
"masterPasswordPolicyRequirementsNotMet": { "masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements." "message": "Your new master password does not meet the policy requirements."
}, },
"receiveMarketingEmails": {
"message": "Get emails from Bitwarden for announcements, advice, and research opportunities."
},
"unsubscribe": {
"message": "Unsubscribe"
},
"atAnyTime": {
"message": "at any time."
},
"byContinuingYouAgreeToThe": {
"message": "By continuing, you agree to the"
},
"and": {
"message": "and"
},
"acceptPolicies": { "acceptPolicies": {
"message": "By checking this box you agree to the following:" "message": "By checking this box you agree to the following:"
}, },

View File

@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base");
config.content = [ config.content = [
"./src/**/*.{html,ts}", "./src/**/*.{html,ts}",
"../../libs/components/src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}",
"../../libs/auth/src/**/*.{html,ts}",
"../../libs/angular/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}",
]; ];

View File

@ -27,7 +27,7 @@ export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent {
} else { } else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/register"], { queryParams: { email: qParams.email } }); this.router.navigate([this.registerRoute], { queryParams: { email: qParams.email } });
} }
} }
} }

View File

@ -29,7 +29,7 @@
<a <a
bitButton bitButton
buttonType="primary" buttonType="primary"
routerLink="/register" [routerLink]="registerRoute"
[queryParams]="{ email: email }" [queryParams]="{ email: email }"
[block]="true" [block]="true"
> >

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router"; import { ActivatedRoute, Params, Router } from "@angular/router";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -28,9 +29,10 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
i18nService: I18nService, i18nService: I18nService,
route: ActivatedRoute, route: ActivatedRoute,
authService: AuthService, authService: AuthService,
configService: ConfigService,
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
) { ) {
super(router, platformUtilsService, i18nService, route, authService); super(router, platformUtilsService, i18nService, route, authService, configService);
} }
async authedHandler(qParams: Params): Promise<void> { async authedHandler(qParams: Params): Promise<void> {

View File

@ -56,7 +56,7 @@
<p class="tw-m-0 tw-text-sm"> <p class="tw-m-0 tw-text-sm">
{{ "newAroundHere" | i18n }} {{ "newAroundHere" | i18n }}
<!--mousedown event is used over click because it prevents the validation from firing --> <!--mousedown event is used over click because it prevents the validation from firing -->
<a routerLink="/register" (mousedown)="goToRegister()">{{ "createAccount" | i18n }}</a> <a bitLink href="#" (mousedown)="goToRegister()">{{ "createAccount" | i18n }}</a>
</p> </p>
</ng-container> </ng-container>

View File

@ -20,6 +20,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -67,6 +68,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
loginEmailService: LoginEmailServiceAbstraction, loginEmailService: LoginEmailServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction,
configService: ConfigService,
) { ) {
super( super(
devicesApiService, devicesApiService,
@ -87,6 +89,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
loginEmailService, loginEmailService,
ssoLoginService, ssoLoginService,
webAuthnLoginService, webAuthnLoginService,
configService,
); );
this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.onSuccessfulLoginNavigate = this.goAfterLogIn;
this.showPasswordless = flagEnabled("showPasswordless"); this.showPasswordless = flagEnabled("showPasswordless");
@ -165,11 +168,11 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
const email = this.formGroup.value.email; const email = this.formGroup.value.email;
if (email) { if (email) {
await this.router.navigate(["/register"], { queryParams: { email: email } }); await this.router.navigate([this.registerRoute], { queryParams: { email: email } });
return; return;
} }
await this.router.navigate(["/register"]); await this.router.navigate([this.registerRoute]);
} }
protected override async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> { protected override async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {

View File

@ -32,7 +32,7 @@
{{ "logIn" | i18n }} {{ "logIn" | i18n }}
</a> </a>
<a <a
routerLink="/register" [routerLink]="registerRoute"
[queryParams]="{ email: email }" [queryParams]="{ email: email }"
class="btn btn-primary btn-block ml-2 mt-0" class="btn btn-primary btn-block ml-2 mt-0"
> >

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router"; import { ActivatedRoute, Params, Router } from "@angular/router";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -23,9 +24,10 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
i18nService: I18nService, i18nService: I18nService,
route: ActivatedRoute, route: ActivatedRoute,
authService: AuthService, authService: AuthService,
configService: ConfigService,
private acceptOrganizationInviteService: AcceptOrganizationInviteService, private acceptOrganizationInviteService: AcceptOrganizationInviteService,
) { ) {
super(router, platformUtilsService, i18nService, route, authService); super(router, platformUtilsService, i18nService, route, authService, configService);
} }
async authedHandler(qParams: Params): Promise<void> { async authedHandler(qParams: Params): Promise<void> {
@ -86,8 +88,26 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
// if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled // if SSO is disabled OR if sso is enabled but the SSO login required policy is not enabled
// then send user to create account // then send user to create account
await this.router.navigate(["/register"], {
queryParams: { email: invite.email, fromOrgInvite: true }, // TODO: update logic when email verification flag is removed
let queryParams: Params;
if (this.registerRoute === "/register") {
queryParams = {
fromOrgInvite: "true",
email: invite.email,
};
} else if (this.registerRoute === "/signup") {
// We have to override the base component route b/c it is correct for other components
// that extend the base accept comp. We don't need users to complete email verification
// if they are coming directly from an emailed org invite.
this.registerRoute = "/finish-signup";
queryParams = {
email: invite.email,
};
}
await this.router.navigate([this.registerRoute], {
queryParams: queryParams,
}); });
return; return;
} }

View File

@ -5,6 +5,8 @@ import { first, switchMap, takeUntil } from "rxjs/operators";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -19,6 +21,9 @@ export abstract class BaseAcceptComponent implements OnInit {
protected failedShortMessage = "inviteAcceptFailedShort"; protected failedShortMessage = "inviteAcceptFailedShort";
protected failedMessage = "inviteAcceptFailed"; protected failedMessage = "inviteAcceptFailed";
// TODO: remove when email verification flag is removed
registerRoute = "/register";
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
@ -27,12 +32,22 @@ export abstract class BaseAcceptComponent implements OnInit {
protected i18nService: I18nService, protected i18nService: I18nService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected authService: AuthService, protected authService: AuthService,
private configService: ConfigService,
) {} ) {}
abstract authedHandler(qParams: Params): Promise<void>; abstract authedHandler(qParams: Params): Promise<void>;
abstract unauthedHandler(qParams: Params): Promise<void>; abstract unauthedHandler(qParams: Params): Promise<void>;
ngOnInit() { async ngOnInit() {
// TODO: remove when email verification flag is removed
const emailVerification = await this.configService.getFeatureFlag(
FeatureFlag.EmailVerification,
);
if (emailVerification) {
this.registerRoute = "/signup";
}
this.route.queryParams this.route.queryParams
.pipe( .pipe(
first(), first(),

View File

@ -9,7 +9,16 @@ import {
UnauthGuard, UnauthGuard,
unauthGuardFn, unauthGuardFn,
} from "@bitwarden/angular/auth/guards"; } from "@bitwarden/angular/auth/guards";
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/auth/angular"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
RegistrationFinishComponent,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { flagEnabled, Flags } from "../utils/flags"; import { flagEnabled, Flags } from "../utils/flags";
@ -175,6 +184,41 @@ const routes: Routes = [
path: "", path: "",
component: AnonLayoutWrapperComponent, component: AnonLayoutWrapperComponent,
children: [ children: [
{
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: { pageTitle: "createAccount", titleId: "createAccount" } satisfies DataProperties &
AnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationStartComponent,
},
{
path: "",
component: RegistrationStartSecondaryComponent,
outlet: "secondary",
data: {
loginRoute: "/login",
} satisfies RegistrationStartSecondaryComponentData,
},
],
},
{
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: "setAStrongPassword",
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
titleId: "setAStrongPassword",
} satisfies DataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationFinishComponent,
},
],
},
{ {
path: "sso", path: "sso",
canActivate: [unauthGuardFn()], canActivate: [unauthGuardFn()],

View File

@ -75,7 +75,7 @@
>Bitwarden Send</a >Bitwarden Send</a
> >
{{ "sendAccessTaglineOr" | i18n }} {{ "sendAccessTaglineOr" | i18n }}
<a bitLink routerLink="/register" target="_blank" rel="noreferrer">{{ <a bitLink [routerLink]="registerRoute" target="_blank" rel="noreferrer">{{
"sendAccessTaglineSignUp" | i18n "sendAccessTaglineSignUp" | i18n
}}</a> }}</a>
{{ "sendAccessTaglineTryToday" | i18n }} {{ "sendAccessTaglineTryToday" | i18n }}

View File

@ -2,7 +2,9 @@ import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -54,6 +56,9 @@ export class AccessComponent implements OnInit {
protected formGroup = this.formBuilder.group({}); protected formGroup = this.formBuilder.group({});
// TODO: remove when email verification flag is removed
registerRoute = "/register";
private id: string; private id: string;
private key: string; private key: string;
@ -64,6 +69,7 @@ export class AccessComponent implements OnInit {
private sendApiService: SendApiService, private sendApiService: SendApiService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
private configService: ConfigService,
protected formBuilder: FormBuilder, protected formBuilder: FormBuilder,
) {} ) {}
@ -81,7 +87,16 @@ export class AccessComponent implements OnInit {
return this.send.creatorIdentifier; return this.send.creatorIdentifier;
} }
ngOnInit() { async ngOnInit() {
// TODO: remove when email verification flag is removed
const emailVerification = await this.configService.getFeatureFlag(
FeatureFlag.EmailVerification,
);
if (emailVerification) {
this.registerRoute = "/signup";
}
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.params.subscribe(async (params) => { this.route.params.subscribe(async (params) => {
this.id = params.sendId; this.id = params.sendId;

View File

@ -713,6 +713,12 @@
"createAccount": { "createAccount": {
"message": "Create account" "message": "Create account"
}, },
"setAStrongPassword": {
"message": "Set a strong password"
},
"finishCreatingYourAccountBySettingAPassword": {
"message": "Finish creating your account by setting a password"
},
"newAroundHere": { "newAroundHere": {
"message": "New around here?" "message": "New around here?"
}, },
@ -3724,6 +3730,21 @@
"nothingSelected": { "nothingSelected": {
"message": "You have not selected anything." "message": "You have not selected anything."
}, },
"receiveMarketingEmails": {
"message": "Get emails from Bitwarden for announcements, advice, and research opportunities."
},
"unsubscribe": {
"message": "Unsubscribe"
},
"atAnyTime": {
"message": "at any time."
},
"byContinuingYouAgreeToThe": {
"message": "By continuing, you agree to the"
},
"and": {
"message": "and"
},
"acceptPolicies": { "acceptPolicies": {
"message": "By checking this box you agree to the following:" "message": "By checking this box you agree to the following:"
}, },

View File

@ -31,7 +31,7 @@
<a <a
bitButton bitButton
buttonType="primary" buttonType="primary"
routerLink="/register" [routerLink]="registerRoute"
[queryParams]="{ email: email }" [queryParams]="{ email: email }"
[block]="true" [block]="true"
> >

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Params, Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
@ -26,8 +27,9 @@ export class AcceptProviderComponent extends BaseAcceptComponent {
authService: AuthService, authService: AuthService,
private apiService: ApiService, private apiService: ApiService,
platformUtilService: PlatformUtilsService, platformUtilService: PlatformUtilsService,
configService: ConfigService,
) { ) {
super(router, platformUtilService, i18nService, route, authService); super(router, platformUtilService, i18nService, route, authService, configService);
} }
async authedHandler(qParams: Params) { async authedHandler(qParams: Params) {

View File

@ -14,7 +14,9 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -56,6 +58,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
protected twoFactorRoute = "2fa"; protected twoFactorRoute = "2fa";
protected successRoute = "vault"; protected successRoute = "vault";
// TODO: remove when email verification flag is removed
protected registerRoute = "/register";
protected forcePasswordResetRoute = "update-temp-password"; protected forcePasswordResetRoute = "update-temp-password";
protected destroy$ = new Subject<void>(); protected destroy$ = new Subject<void>();
@ -83,11 +87,21 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
protected loginEmailService: LoginEmailServiceAbstraction, protected loginEmailService: LoginEmailServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction,
protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, protected webAuthnLoginService: WebAuthnLoginServiceAbstraction,
protected configService: ConfigService,
) { ) {
super(environmentService, i18nService, platformUtilsService); super(environmentService, i18nService, platformUtilsService);
} }
async ngOnInit() { async ngOnInit() {
// TODO: remove when email verification flag is removed
const emailVerification = await this.configService.getFeatureFlag(
FeatureFlag.EmailVerification,
);
if (emailVerification) {
this.registerRoute = "/signup";
}
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
if (!params) { if (!params) {
return; return;

View File

@ -486,6 +486,7 @@ const safeProviders: SafeProvider[] = [
UserVerificationServiceAbstraction, UserVerificationServiceAbstraction,
LogService, LogService,
InternalAccountService, InternalAccountService,
EnvironmentService,
], ],
}), }),
safeProvider({ safeProvider({

View File

@ -1,5 +1,6 @@
import { Component } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router"; import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
import { Subject, filter, switchMap, takeUntil, tap } from "rxjs";
import { AnonLayoutComponent } from "@bitwarden/auth/angular"; import { AnonLayoutComponent } from "@bitwarden/auth/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -17,31 +18,64 @@ export interface AnonLayoutWrapperData {
templateUrl: "anon-layout-wrapper.component.html", templateUrl: "anon-layout-wrapper.component.html",
imports: [AnonLayoutComponent, RouterModule], imports: [AnonLayoutComponent, RouterModule],
}) })
export class AnonLayoutWrapperComponent { export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected pageTitle: string; protected pageTitle: string;
protected pageSubtitle: string; protected pageSubtitle: string;
protected pageIcon: Icon; protected pageIcon: Icon;
protected showReadonlyHostname: boolean; protected showReadonlyHostname: boolean;
constructor( constructor(
private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private i18nService: I18nService, private i18nService: I18nService,
) { ) {}
const routeData = this.route.snapshot.firstChild?.data;
if (!routeData) { ngOnInit(): void {
// Set the initial page data on load
this.setAnonLayoutWrapperData(this.route.snapshot.firstChild?.data);
// Listen for page changes and update the page data appropriately
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
// reset page data on page changes
tap(() => this.resetPageData()),
switchMap(() => this.route.firstChild?.data || null),
takeUntil(this.destroy$),
)
.subscribe((firstChildRouteData: Data | null) => {
this.setAnonLayoutWrapperData(firstChildRouteData);
});
}
private setAnonLayoutWrapperData(firstChildRouteData: Data | null) {
if (!firstChildRouteData) {
return; return;
} }
if (routeData["pageTitle"] !== undefined) { if (firstChildRouteData["pageTitle"] !== undefined) {
this.pageTitle = this.i18nService.t(routeData["pageTitle"]); this.pageTitle = this.i18nService.t(firstChildRouteData["pageTitle"]);
} }
if (routeData["pageSubtitle"] !== undefined) { if (firstChildRouteData["pageSubtitle"] !== undefined) {
this.pageSubtitle = this.i18nService.t(routeData["pageSubtitle"]); this.pageSubtitle = this.i18nService.t(firstChildRouteData["pageSubtitle"]);
} }
this.pageIcon = routeData["pageIcon"]; this.pageIcon = firstChildRouteData["pageIcon"];
this.showReadonlyHostname = routeData["showReadonlyHostname"]; this.showReadonlyHostname = firstChildRouteData["showReadonlyHostname"];
}
private resetPageData() {
this.pageTitle = null;
this.pageSubtitle = null;
this.pageIcon = null;
this.showReadonlyHostname = null;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
} }
} }

View File

@ -17,5 +17,6 @@ export * from "./user-verification/user-verification-form-input.component";
// registration // registration
export * from "./registration/registration-start/registration-start.component"; export * from "./registration/registration-start/registration-start.component";
export * from "./registration/registration-finish/registration-finish.component";
export * from "./registration/registration-start/registration-start-secondary.component"; export * from "./registration/registration-start/registration-start-secondary.component";
export * from "./registration/registration-env-selector/registration-env-selector.component"; export * from "./registration/registration-env-selector/registration-env-selector.component";

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 // Save this off so we can reset the value to the previously selected region
// if the self hosted settings are closed without saving. // if the self hosted settings are closed without saving.
this.selectedRegionFromEnv = selectedRegionFromEnv; this.selectedRegionFromEnv = selectedRegionFromEnv;
// Emit the initial value
this.selectedRegionChange.emit(selectedRegionFromEnv);
}), }),
takeUntil(this.destroy$), 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 <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 { CommonModule } from "@angular/common";
import { Component } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router"; import { ActivatedRoute, RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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({ @Component({
standalone: true, standalone: true,
selector: "auth-registration-start-secondary", selector: "auth-registration-start-secondary",
templateUrl: "./registration-start-secondary.component.html", templateUrl: "./registration-start-secondary.component.html",
imports: [CommonModule, JslibModule, RouterModule], imports: [CommonModule, JslibModule, RouterModule],
}) })
export class RegistrationStartSecondaryComponent { export class RegistrationStartSecondaryComponent implements OnInit {
constructor() {} 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"> <bit-form-control *ngIf="!isSelfHost">
<input <input
id="register-start-form-input-accept-policies" id="register-start-form-input-receive-marketing-emails"
type="checkbox" type="checkbox"
bitCheckbox bitCheckbox
formControlName="acceptPolicies" formControlName="receiveMarketingEmails"
/> />
<bit-label for="register-start-form-input-accept-policies"> <bit-label for="register-start-form-input-receive-marketing-emails">
{{ "acceptPolicies" | i18n }} {{ "receiveMarketingEmails" | i18n }}
<a <a
bitLink bitLink
linkType="primary" linkType="primary"
href="https://bitwarden.com/terms/" href="https://bitwarden.com/email-preferences"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>{{ "termsOfService" | i18n }}</a >{{ "unsubscribe" | i18n }}</a
>,
<a
bitLink
linkType="primary"
href="https://bitwarden.com/privacy/"
target="_blank"
rel="noreferrer"
>{{ "privacyPolicy" | i18n }}</a
> >
{{ "atAnyTime" | i18n }}
</bit-label> </bit-label>
</bit-form-control> </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 }} {{ "continue" | i18n }}
</button> </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 <bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary></form
></ng-container> ></ng-container>
<ng-container *ngIf="state === RegistrationStartState.CHECK_EMAIL"> <ng-container *ngIf="state === RegistrationStartState.CHECK_EMAIL">
<div class="tw-flex tw-flex-col tw-items-center tw-justify-center"> <div class="tw-flex tw-flex-col tw-items-center tw-justify-center">
<bit-icon [icon]="Icons.RegistrationCheckEmailIcon" class="tw-mb-6"></bit-icon> <bit-icon [icon]="Icons.RegistrationCheckEmailIcon" class="tw-mb-6"></bit-icon>

View File

@ -1,17 +1,12 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
import { import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
AbstractControl, import { ActivatedRoute, Router } from "@angular/router";
FormBuilder,
FormControl,
ReactiveFormsModule,
ValidatorFn,
Validators,
} from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { RegionConfig, Region } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { import {
@ -31,6 +26,12 @@ export enum RegistrationStartState {
CHECK_EMAIL = "CheckEmail", CHECK_EMAIL = "CheckEmail",
} }
const DEFAULT_MARKETING_EMAILS_PREF_BY_REGION: Record<Region, boolean> = {
[Region.US]: true,
[Region.EU]: false,
[Region.SelfHosted]: false,
};
@Component({ @Component({
standalone: true, standalone: true,
selector: "auth-registration-start", selector: "auth-registration-start",
@ -60,20 +61,19 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
formGroup = this.formBuilder.group({ formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]], email: ["", [Validators.required, Validators.email]],
name: [""], name: [""],
acceptPolicies: [false, [this.acceptPoliciesValidator()]], receiveMarketingEmails: [false],
selectedRegion: [null],
}); });
get email(): FormControl { get email() {
return this.formGroup.get("email") as FormControl; return this.formGroup.controls.email;
} }
get name(): FormControl { get name() {
return this.formGroup.get("name") as FormControl; return this.formGroup.controls.name;
} }
get acceptPolicies(): FormControl { get receiveMarketingEmails() {
return this.formGroup.get("acceptPolicies") as FormControl; return this.formGroup.controls.receiveMarketingEmails;
} }
emailReadonly: boolean = false; emailReadonly: boolean = false;
@ -86,8 +86,9 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private route: ActivatedRoute, private route: ActivatedRoute,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private accountApiService: AccountApiService,
private router: Router,
) { ) {
// TODO: this needs to update if user selects self hosted
this.isSelfHost = platformUtilsService.isSelfHost(); 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 () => { submit = async () => {
const valid = this.validateForm(); const valid = this.validateForm();
@ -114,14 +127,31 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
return; 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.state = RegistrationStartState.CHECK_EMAIL;
this.registrationStartStateChange.emit(this.state); this.registrationStartStateChange.emit(this.state);
}; };
handleSelectedRegionChange(region: RegionConfig | Region.SelfHosted | null) { handleSelectedRegionChange(region: RegionConfig | Region.SelfHosted | null) {
this.isSelfHost = region === Region.SelfHosted; this.isSelfHost = region === Region.SelfHosted;
if (region !== null) {
this.setReceiveMarketingEmailsByRegion(region);
}
} }
private validateForm(): boolean { private validateForm(): boolean {
@ -139,14 +169,6 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
this.registrationStartStateChange.emit(this.state); this.registrationStartStateChange.emit(this.state);
} }
private acceptPoliciesValidator(): ValidatorFn {
return (control: AbstractControl) => {
const ctrlValue = control.value;
return !ctrlValue && !this.isSelfHost ? { required: true } : null;
};
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); 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 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 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 email address (required) and optionally their name. On cloud environments, it offers a checkbox for
the terms of service and the privacy policy; the checkbox is hidden on self hosted environments. 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 ## Web Examples
@ -36,8 +37,10 @@ field will be set to readonly. `emailReadonly` is primarily for the organization
Behavior to note: Behavior to note:
- The self hosted option is present in the environment selector. - 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 - If you go from non-self hosted to self hosted, the receive marketing emails checkbox will
will disappear. disappear.
- If you change regions, the receive marketing emails checkbox default value will change based on
the region.
### US Region ### US Region
@ -49,8 +52,8 @@ Behavior to note:
### Self Hosted ### Self Hosted
Note the fact that the terms of service and privacy policy checkbox is not present when the Note the fact that the receive marketing emails checkbox is not present when the environment is self
environment is self hosted. hosted.
<Story of={stories.DesktopSelfHostExample} /> <Story of={stories.DesktopSelfHostExample} />
@ -59,8 +62,10 @@ environment is self hosted.
Behavior to note: Behavior to note:
- The self hosted option is present in the environment selector. - 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 - If you go from non-self hosted to self hosted, the receive marketing emails checkbox will
will disappear. disappear.
- If you change regions, the receive marketing emails checkbox default value will change based on
the region.
### US Region ### US Region
@ -72,7 +77,7 @@ Behavior to note:
### Self Hosted ### Self Hosted
Note the fact that the terms of service and privacy policy checkbox is not present when the Note the fact that the receive marketing emails checkbox is not present when the environment is self
environment is self hosted. hosted.
<Story of={stories.BrowserExtensionSelfHostExample} /> <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 { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { of } from "rxjs"; import { of } from "rxjs";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { ClientType } from "@bitwarden/common/enums"; import { ClientType } from "@bitwarden/common/enums";
import { import {
Environment, Environment,
@ -53,7 +54,6 @@ const decorators = (options: {
LinkModule, LinkModule,
TypographyModule, TypographyModule,
AsyncActionsModule, AsyncActionsModule,
BrowserAnimationsModule,
], ],
providers: [ providers: [
{ {
@ -64,6 +64,7 @@ const decorators = (options: {
}), }),
applicationConfig({ applicationConfig({
providers: [ providers: [
importProvidersFrom(BrowserAnimationsModule),
importProvidersFrom(PreloadedEnglishI18nModule), importProvidersFrom(PreloadedEnglishI18nModule),
{ {
provide: EnvironmentService, provide: EnvironmentService,
@ -91,6 +92,12 @@ const decorators = (options: {
showToast: (options: ToastOptions) => {}, showToast: (options: ToastOptions) => {},
} as Partial<ToastService>, } as Partial<ToastService>,
}, },
{
provide: AccountApiService,
useValue: {
registerSendVerificationEmail: () => Promise.resolve(null),
} as Partial<AccountApiService>,
},
], ],
}), }),
]; ];

View File

@ -1,5 +1,27 @@
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
import { Verification } from "../types/verification"; import { Verification } from "../types/verification";
export abstract class AccountApiService { export abstract class AccountApiService {
/**
* Deletes an account that has confirmed the operation is authorized
*
* @param verification - authorizes the account deletion operation.
* @returns A promise that resolves when the account is
* successfully deleted.
*/
abstract deleteAccount(verification: Verification): Promise<void>; abstract deleteAccount(verification: Verification): Promise<void>;
/**
* Sends a verification email as part of the registration process.
*
* @param request - The request object containing
* information needed to send the verification email, such as the user's email address.
* @returns A promise that resolves to a string tokencontaining the user's encrypted
* information which must be submitted to complete registration or `null` if
* email verification is enabled (users must get the token by clicking a
* link in the email that will be sent to them).
*/
abstract registerSendVerificationEmail(
request: RegisterSendVerificationEmailRequest,
): Promise<null | string>;
} }

View File

@ -0,0 +1,7 @@
export class RegisterSendVerificationEmailRequest {
constructor(
public email: string,
public name: string,
public receiveMarketingEmails: boolean,
) {}
}

View File

@ -1,8 +1,13 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { ErrorResponse } from "../../models/response/error.response";
import { EnvironmentService } from "../../platform/abstractions/environment.service";
import { LogService } from "../../platform/abstractions/log.service"; import { LogService } from "../../platform/abstractions/log.service";
import { AccountApiService } from "../abstractions/account-api.service"; import { AccountApiService } from "../abstractions/account-api.service";
import { InternalAccountService } from "../abstractions/account.service"; import { InternalAccountService } from "../abstractions/account.service";
import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction";
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
import { Verification } from "../types/verification"; import { Verification } from "../types/verification";
export class AccountApiServiceImplementation implements AccountApiService { export class AccountApiServiceImplementation implements AccountApiService {
@ -11,6 +16,7 @@ export class AccountApiServiceImplementation implements AccountApiService {
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
private logService: LogService, private logService: LogService,
private accountService: InternalAccountService, private accountService: InternalAccountService,
private environmentService: EnvironmentService,
) {} ) {}
async deleteAccount(verification: Verification): Promise<void> { async deleteAccount(verification: Verification): Promise<void> {
@ -23,4 +29,33 @@ export class AccountApiServiceImplementation implements AccountApiService {
throw e; throw e;
} }
} }
async registerSendVerificationEmail(
request: RegisterSendVerificationEmailRequest,
): Promise<null | string> {
const env = await firstValueFrom(this.environmentService.environment$);
try {
const response = await this.apiService.send(
"POST",
"/accounts/register/send-verification-email",
request,
false,
true,
env.getIdentityUrl(),
);
return response;
} catch (e: unknown) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 204) {
// No content is a success response.
return null;
}
}
this.logService.error(e);
throw e;
}
}
} }

View File

@ -17,6 +17,7 @@ export enum FeatureFlag {
RestrictProviderAccess = "restrict-provider-access", RestrictProviderAccess = "restrict-provider-access",
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
BulkDeviceApproval = "bulk-device-approval", BulkDeviceApproval = "bulk-device-approval",
EmailVerification = "email-verification",
} }
export type AllowedFeatureFlagTypes = boolean | number | string; export type AllowedFeatureFlagTypes = boolean | number | string;
@ -44,6 +45,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.RestrictProviderAccess]: FALSE, [FeatureFlag.RestrictProviderAccess]: FALSE,
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
[FeatureFlag.BulkDeviceApproval]: FALSE, [FeatureFlag.BulkDeviceApproval]: FALSE,
[FeatureFlag.EmailVerification]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>; } satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;