mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
Auth/PM-8112 - UI refresh - Registration Components (#11353)
* PM-8112 - Update classes of existing registration icons * PM-8112 - Add new icons * PM-8112 - Export icons from libs/auth * PM-8112 - RegistrationStart - Add new user icon as page icon * PM-8112 - Replace RegistrationCheckEmailIcon with new icon so it displays properly * PM-8112 - RegistrationFinish - Add new icon across clients * PM-8112 - Registration start comp - update page icon and page title on state change to match figma * PM-8112 - RegistrationFinish - adding most of framework for changing page title & subtitle when an org invite is in state. * PM-8112 - Add joinOrganizationName to all clients translations * PM-8112 - RegistrationFinish - Remove default page title & subtitle and let onInit logic figure out what to set based on flows. * PM-8112 - RegistrationStart - Fix setAnonLayoutWrapperData calls * PM-8112 - RegistrationFinish - simplify qParams init logic to make handling loading and page title and subtitle setting easier. * PM-8112 - Registration Link expired - move icon to page icon out of main content * PM-8112 - RegistrationFinish - Refactor init logic further into distinct flows. * PM-8112 - Fix icons * PM-8112 - Extension AppRoutingModule - move sign up start & finish routes under extension anon layout * PM-8112 - Fix storybook * PM-8112 - Clean up unused prop * PM-8112 - RegistrationLockAltIcon tweaks * PM-8112 - Update icons to have proper styling * PM-8112 - RegistrationUserAddIcon - remove unnecessary svg class * PM-8112 - Fix icons
This commit is contained in:
@@ -37,6 +37,14 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrgNameFromOrgInvite()", () => {
|
||||
it("returns null", async () => {
|
||||
const result = await service.getOrgNameFromOrgInvite();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("finishRegistration()", () => {
|
||||
let email: string;
|
||||
let emailVerificationToken: string;
|
||||
|
||||
@@ -15,6 +15,10 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
protected accountApiService: AccountApiService,
|
||||
) {}
|
||||
|
||||
getOrgNameFromOrgInvite(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
getMasterPasswordPolicyOptsFromOrgInvite(): Promise<MasterPasswordPolicyOptions | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router, RouterModule } from "@angular/router";
|
||||
import { EMPTY, Subject, from, switchMap, takeUntil, tap } from "rxjs";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
@@ -15,6 +15,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common";
|
||||
import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { InputPasswordComponent } from "../../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../../input-password/password-input-result";
|
||||
|
||||
@@ -60,55 +61,72 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
private accountApiService: AccountApiService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.listenForQueryParamChanges();
|
||||
this.masterPasswordPolicyOptions =
|
||||
await this.registrationFinishService.getMasterPasswordPolicyOptsFromOrgInvite();
|
||||
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
this.handleQueryParams(qParams);
|
||||
|
||||
if (
|
||||
qParams.fromEmail &&
|
||||
qParams.fromEmail === "true" &&
|
||||
this.email &&
|
||||
this.emailVerificationToken
|
||||
) {
|
||||
await this.initEmailVerificationFlow();
|
||||
} else {
|
||||
// Org Invite flow OR registration with email verification disabled Flow
|
||||
const orgInviteFlow = await this.initOrgInviteFlowIfPresent();
|
||||
|
||||
if (!orgInviteFlow) {
|
||||
this.initRegistrationWithEmailVerificationDisabledFlow();
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private listenForQueryParamChanges() {
|
||||
this.activatedRoute.queryParams
|
||||
.pipe(
|
||||
tap((qParams: Params) => {
|
||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
private handleQueryParams(qParams: Params) {
|
||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
|
||||
if (qParams.token != null) {
|
||||
this.emailVerificationToken = qParams.token;
|
||||
}
|
||||
if (qParams.token != null) {
|
||||
this.emailVerificationToken = qParams.token;
|
||||
}
|
||||
|
||||
if (qParams.orgSponsoredFreeFamilyPlanToken != null) {
|
||||
this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken;
|
||||
}
|
||||
if (qParams.orgSponsoredFreeFamilyPlanToken != null) {
|
||||
this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken;
|
||||
}
|
||||
|
||||
if (qParams.acceptEmergencyAccessInviteToken != null && qParams.emergencyAccessId) {
|
||||
this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken;
|
||||
this.emergencyAccessId = qParams.emergencyAccessId;
|
||||
}
|
||||
}),
|
||||
switchMap((qParams: Params) => {
|
||||
if (
|
||||
qParams.fromEmail &&
|
||||
qParams.fromEmail === "true" &&
|
||||
this.email &&
|
||||
this.emailVerificationToken
|
||||
) {
|
||||
return from(
|
||||
this.registerVerificationEmailClicked(this.email, this.emailVerificationToken),
|
||||
);
|
||||
} else {
|
||||
// org invite flow
|
||||
this.loading = false;
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
if (qParams.acceptEmergencyAccessInviteToken != null && qParams.emergencyAccessId) {
|
||||
this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken;
|
||||
this.emergencyAccessId = qParams.emergencyAccessId;
|
||||
}
|
||||
}
|
||||
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
private async initOrgInviteFlowIfPresent(): Promise<boolean> {
|
||||
this.masterPasswordPolicyOptions =
|
||||
await this.registrationFinishService.getMasterPasswordPolicyOptsFromOrgInvite();
|
||||
|
||||
const orgName = await this.registrationFinishService.getOrgNameFromOrgInvite();
|
||||
if (orgName) {
|
||||
// Org invite exists
|
||||
// Set the page title and subtitle appropriately
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: {
|
||||
key: "joinOrganizationName",
|
||||
placeholders: [orgName],
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "finishJoiningThisOrganizationBySettingAMasterPassword",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
@@ -162,9 +180,24 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
this.submitting = false;
|
||||
}
|
||||
|
||||
private setDefaultPageTitleAndSubtitle() {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: {
|
||||
key: "setAStrongPassword",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "finishCreatingYourAccountBySettingAPassword",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async initEmailVerificationFlow() {
|
||||
this.setDefaultPageTitleAndSubtitle();
|
||||
await this.registerVerificationEmailClicked(this.email, this.emailVerificationToken);
|
||||
}
|
||||
|
||||
private async registerVerificationEmailClicked(email: string, emailVerificationToken: string) {
|
||||
const request = new RegisterVerificationEmailClickedRequest(email, emailVerificationToken);
|
||||
|
||||
try {
|
||||
const result = await this.accountApiService.registerVerificationEmailClicked(request);
|
||||
|
||||
@@ -174,11 +207,9 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
message: this.i18nService.t("emailVerifiedV2"),
|
||||
variant: "success",
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
await this.handleRegisterVerificationEmailClickedError(e);
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +235,10 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private initRegistrationWithEmailVerificationDisabledFlow() {
|
||||
this.setDefaultPageTitleAndSubtitle();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
@@ -3,6 +3,13 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
import { PasswordInputResult } from "../../input-password/password-input-result";
|
||||
|
||||
export abstract class RegistrationFinishService {
|
||||
/**
|
||||
* Retrieves the organization name from an organization invite if it exists.
|
||||
* Organization invites can currently only be accepted on the web.
|
||||
* @returns a promise which resolves to the organization name string or null if no invite exists.
|
||||
*/
|
||||
abstract getOrgNameFromOrgInvite(): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Gets the master password policy options from an organization invite if it exits.
|
||||
* Organization invites can currently only be accepted on the web.
|
||||
@@ -18,7 +25,7 @@ export abstract class RegistrationFinishService {
|
||||
* @param orgSponsoredFreeFamilyPlanToken The optional org sponsored free family plan token.
|
||||
* @param acceptEmergencyAccessInviteToken The optional accept emergency access invite token.
|
||||
* @param emergencyAccessId The optional emergency access id which is required to validate the emergency access invite token.
|
||||
* Returns a promise which resolves to the captcha bypass token string upon a successful account creation.
|
||||
* @returns a promise which resolves to the captcha bypass token string upon a successful account creation.
|
||||
*/
|
||||
abstract finishRegistration(
|
||||
email: string,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-justify-center">
|
||||
<bit-icon [icon]="Icons.RegistrationExpiredLinkIcon" class="tw-mb-6"></bit-icon>
|
||||
|
||||
<p
|
||||
bitTypography="body1"
|
||||
class="tw-text-center tw-mb-3 tw-text-main"
|
||||
|
||||
@@ -79,19 +79,6 @@
|
||||
|
||||
<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>
|
||||
|
||||
<h2
|
||||
bitTypography="h2"
|
||||
id="check_your_email_heading"
|
||||
class="tw-font-bold tw-mb-3 tw-text-main"
|
||||
tabindex="0"
|
||||
aria-describedby="follow_the_link_body"
|
||||
appAutofocus
|
||||
>
|
||||
{{ "checkYourEmail" | i18n }}
|
||||
</h2>
|
||||
|
||||
<p bitTypography="body1" class="tw-text-center tw-mb-3 tw-text-main" id="follow_the_link_body">
|
||||
{{ "followTheLinkInTheEmailSentTo" | i18n }}
|
||||
<span class="tw-font-bold">{{ email.value }}</span>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { RegistrationUserAddIcon } from "../../icons";
|
||||
import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon";
|
||||
import { RegistrationEnvSelectorComponent } from "../registration-env-selector/registration-env-selector.component";
|
||||
|
||||
@@ -54,7 +56,6 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
|
||||
state: RegistrationStartState = RegistrationStartState.USER_DATA_ENTRY;
|
||||
RegistrationStartState = RegistrationStartState;
|
||||
readonly Icons = { RegistrationCheckEmailIcon };
|
||||
|
||||
isSelfHost = false;
|
||||
|
||||
@@ -88,6 +89,7 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private accountApiService: AccountApiService,
|
||||
private router: Router,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
) {
|
||||
this.isSelfHost = platformUtilsService.isSelfHost();
|
||||
}
|
||||
@@ -148,6 +150,12 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Result is null, so email verification is required
|
||||
this.state = RegistrationStartState.CHECK_EMAIL;
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: {
|
||||
key: "checkYourEmail",
|
||||
},
|
||||
pageIcon: RegistrationCheckEmailIcon,
|
||||
});
|
||||
this.registrationStartStateChange.emit(this.state);
|
||||
};
|
||||
|
||||
@@ -171,6 +179,12 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
|
||||
goBack() {
|
||||
this.state = RegistrationStartState.USER_DATA_ENTRY;
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageIcon: RegistrationUserAddIcon,
|
||||
pageTitle: {
|
||||
key: "createAccount",
|
||||
},
|
||||
});
|
||||
this.registrationStartStateChange.emit(this.state);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
// FIXME: remove `/apps` import from `/libs`
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests";
|
||||
import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { AnonLayoutWrapperData } from "../../anon-layout/anon-layout-wrapper.component";
|
||||
|
||||
import { RegistrationStartComponent } from "./registration-start.component";
|
||||
|
||||
@@ -88,6 +90,14 @@ const decorators = (options: {
|
||||
getClientType: () => options.clientType || ClientType.Web,
|
||||
} as Partial<PlatformUtilsService>,
|
||||
},
|
||||
{
|
||||
provide: AnonLayoutWrapperDataService,
|
||||
useValue: {
|
||||
setAnonLayoutWrapperData: (data: AnonLayoutWrapperData) => {
|
||||
return;
|
||||
},
|
||||
} as Partial<AnonLayoutWrapperDataService>,
|
||||
},
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: {
|
||||
|
||||
Reference in New Issue
Block a user