1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-22 07:50:04 +02: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:
Jared Snider 2024-10-16 18:28:27 -04:00 committed by GitHub
parent e256bde1de
commit 4b67cd24b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 336 additions and 141 deletions

View File

@ -71,6 +71,15 @@
"joinOrganization": {
"message": "Join organization"
},
"joinOrganizationName": {
"message": "Join $ORGANIZATIONNAME$",
"placeholders": {
"organizationName": {
"content": "$1",
"example": "My Org Name"
}
}
},
"finishJoiningThisOrganizationBySettingAMasterPassword": {
"message": "Finish joining this organization by setting a master password."
},

View File

@ -20,9 +20,11 @@ import {
LockV2Component,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationLockAltIcon,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
RegistrationUserAddIcon,
SetPasswordJitComponent,
UserLockIcon,
} from "@bitwarden/auth/angular";
@ -448,6 +450,47 @@ const routes: Routes = [
path: "",
component: ExtensionAnonLayoutWrapperComponent,
children: [
{
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
state: "signup",
pageIcon: RegistrationUserAddIcon,
pageTitle: {
key: "createAccount",
},
showBackButton: true,
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationStartComponent,
},
{
path: "",
component: RegistrationStartSecondaryComponent,
outlet: "secondary",
data: {
loginRoute: "/home",
} satisfies RegistrationStartSecondaryComponentData,
},
],
},
{
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageIcon: RegistrationLockAltIcon,
state: "finish-signup",
showBackButton: true,
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationFinishComponent,
},
],
},
{
path: "lockV2",
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
@ -472,49 +515,6 @@ const routes: Routes = [
path: "",
component: AnonLayoutWrapperComponent,
children: [
{
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
state: "signup",
pageTitle: {
key: "createAccount",
},
} satisfies RouteDataProperties & 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: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
state: "finish-signup",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: RegistrationFinishComponent,
},
],
},
{
path: "set-password-jit",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],

View File

@ -19,9 +19,11 @@ import {
LockV2Component,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationLockAltIcon,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
RegistrationUserAddIcon,
SetPasswordJitComponent,
UserLockIcon,
} from "@bitwarden/auth/angular";
@ -169,6 +171,7 @@ const routes: Routes = [
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageIcon: RegistrationUserAddIcon,
pageTitle: {
key: "createAccount",
},
@ -192,12 +195,7 @@ const routes: Routes = [
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
pageIcon: RegistrationLockAltIcon,
} satisfies AnonLayoutWrapperData,
children: [
{

View File

@ -602,6 +602,15 @@
"joinOrganization": {
"message": "Join organization"
},
"joinOrganizationName": {
"message": "Join $ORGANIZATIONNAME$",
"placeholders": {
"organizationName": {
"content": "$1",
"example": "My Org Name"
}
}
},
"finishJoiningThisOrganizationBySettingAMasterPassword": {
"message": "Finish joining this organization by setting a master password."
},

View File

@ -52,6 +52,36 @@ describe("DefaultRegistrationFinishService", () => {
expect(service).not.toBeFalsy();
});
describe("getOrgNameFromOrgInvite()", () => {
let orgInvite: OrganizationInvite | null;
beforeEach(() => {
orgInvite = new OrganizationInvite();
orgInvite.organizationId = "organizationId";
orgInvite.organizationUserId = "organizationUserId";
orgInvite.token = "orgInviteToken";
orgInvite.email = "email";
});
it("returns null when the org invite is null", async () => {
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.getOrgNameFromOrgInvite();
expect(result).toBeNull();
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
});
it("returns the organization name from the organization invite when it exists", async () => {
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
const result = await service.getOrgNameFromOrgInvite();
expect(result).toEqual(orgInvite.organizationName);
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
});
});
describe("getMasterPasswordPolicyOptsFromOrgInvite()", () => {
let orgInvite: OrganizationInvite | null;

View File

@ -32,6 +32,15 @@ export class WebRegistrationFinishService
super(cryptoService, accountApiService);
}
override async getOrgNameFromOrgInvite(): Promise<string | null> {
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
if (orgInvite == null) {
return null;
}
return orgInvite.organizationName;
}
override async getMasterPasswordPolicyOptsFromOrgInvite(): Promise<MasterPasswordPolicyOptions | null> {
// If there's a deep linked org invite, use it to get the password policies
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();

View File

@ -25,6 +25,9 @@ import {
LockV2Component,
LockIcon,
UserLockIcon,
RegistrationUserAddIcon,
RegistrationLockAltIcon,
RegistrationExpiredLinkIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@ -234,6 +237,7 @@ const routes: Routes = [
path: "signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageIcon: RegistrationUserAddIcon,
pageTitle: {
key: "createAccount",
},
@ -258,12 +262,7 @@ const routes: Routes = [
path: "finish-signup",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageTitle: {
key: "setAStrongPassword",
},
pageSubtitle: {
key: "finishCreatingYourAccountBySettingAPassword",
},
pageIcon: RegistrationLockAltIcon,
titleId: "setAStrongPassword",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
@ -310,6 +309,7 @@ const routes: Routes = [
path: "signup-link-expired",
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
data: {
pageIcon: RegistrationExpiredLinkIcon,
pageTitle: {
key: "expiredLink",
},

View File

@ -3781,6 +3781,15 @@
"joinOrganization": {
"message": "Join organization"
},
"joinOrganizationName": {
"message": "Join $ORGANIZATIONNAME$",
"placeholders": {
"organizationName": {
"content": "$1",
"example": "My Org Name"
}
}
},
"joinOrganizationDesc": {
"message": "You've been invited to join the organization listed above. To accept the invitation, you need to log in or create a new Bitwarden account."
},

View File

@ -3,3 +3,6 @@ export * from "./bitwarden-shield.icon";
export * from "./lock.icon";
export * from "./user-lock.icon";
export * from "./user-verification-biometrics-fingerprint.icon";
export * from "./registration-user-add.icon";
export * from "./registration-lock-alt.icon";
export * from "./registration-expired-link.icon";

View File

@ -1,12 +1,23 @@
import { svgIcon } from "@bitwarden/components";
export const RegistrationCheckEmailIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="144" height="113" fill="none">
<g fill="#175DDC" clip-path="url(#a)">
<path class="tw-fill-primary-600" fill-rule="evenodd" d="M39.19 26.64c0-.765.62-1.384 1.383-1.384h18.785a1.383 1.383 0 0 1 0 2.766H40.573c-.764 0-1.383-.619-1.383-1.383ZM39.19 37.819c0-.764.62-1.383 1.383-1.383H58.81a1.383 1.383 0 0 1 0 2.766H40.573c-.764 0-1.383-.62-1.383-1.383ZM39.19 48.998c0-.764.62-1.383 1.383-1.383H61.82a1.383 1.383 0 0 1 0 2.766H40.573c-.764 0-1.383-.62-1.383-1.383Z" clip-rule="evenodd"/>
<path class="tw-fill-primary-600" d="M94.576 60.504c0 .382.31.692.691.692 14.892 0 26.977-11.935 26.977-26.675a.692.692 0 0 0-1.383 0c0 13.96-11.451 25.292-25.594 25.292a.691.691 0 0 0-.691.691ZM68.982 35.213c.382 0 .692-.31.692-.692 0-13.96 11.45-25.291 25.593-25.291a.691.691 0 1 0 0-1.383c-14.89 0-26.976 11.935-26.976 26.674 0 .382.31.692.691.692Z"/>
<path class="tw-fill-primary-600" fill-rule="evenodd" d="M95.387 2.926c-6.295 0-12.16 1.84-17.086 5.013l-1.136-.877a9.68 9.68 0 0 0-11.705-.096l-8.172 6.098a1.33 1.33 0 0 0-.064.05H33.393a4.149 4.149 0 0 0-4.149 4.15v16.753c-.07.035-.14.078-.205.127l-8.464 6.315a9.68 9.68 0 0 0-3.89 7.759v51.696c0 5.346 4.333 9.68 9.68 9.68h90.733c5.346 0 9.68-4.334 9.68-9.68v-26.48l8.099 8.098a4.84 4.84 0 0 0 6.845 0l.721-.721a4.84 4.84 0 0 0 0-6.845l-15.665-15.665V44.056c0-.587-.366-1.089-.882-1.29a31.666 31.666 0 0 0 1.086-8.245c0-17.45-14.146-31.595-31.595-31.595Zm28.625 52.61v-7.624a31.463 31.463 0 0 1-2.803 4.82l2.803 2.803Zm-4.51-.6a31.84 31.84 0 0 1-3.651 3.659l20.981 20.982c.81.81 2.124.81 2.934 0l.721-.722a2.073 2.073 0 0 0 0-2.933l-20.985-20.986Zm-5.851 5.37a31.434 31.434 0 0 1-16.653 5.77l-.023.014-9.463 5.427a15.205 15.205 0 0 1 2.167 1.666l33.149 30.601a6.876 6.876 0 0 0 1.184-3.87V70.668l-10.361-10.362ZM29.244 37.442l-7.014 5.234a6.914 6.914 0 0 0-2.588 3.925c.062.025.123.056.183.091l9.42 5.626V37.442Zm2.766 16.527 8.172 4.881c.124-.036.255-.056.39-.056h27.813a1.383 1.383 0 0 1 0 2.766H44.72l13.656 8.156a15.208 15.208 0 0 1 4.116-.567H79.36c1.801 0 3.571.32 5.233.928.095-.103.206-.193.334-.267l6.805-3.902C76.004 64.097 63.791 50.735 63.791 34.52a31.454 31.454 0 0 1 6.082-18.64h-36.48c-.764 0-1.383.619-1.383 1.383v36.705Zm40.135-40.851a31.78 31.78 0 0 1 3.8-3.504l-.47-.363a6.914 6.914 0 0 0-8.36-.068l-5.27 3.932h10.212c.03 0 .059 0 .088.003ZM66.557 34.52c0-15.922 12.907-28.83 28.83-28.83 15.922 0 28.829 12.908 28.829 28.83 0 15.922-12.907 28.83-28.83 28.83-15.922 0-28.83-12.908-28.83-28.83ZM19.45 49.691v50.223c0 1.418.428 2.738 1.16 3.835l31.298-30.315a15.21 15.21 0 0 1 3.266-2.41L19.45 49.69Zm3.124 56.007a6.888 6.888 0 0 0 3.79 1.13h90.734a6.879 6.879 0 0 0 3.752-1.106L87.803 75.216a12.446 12.446 0 0 0-8.442-3.301H62.49a12.446 12.446 0 0 0-8.658 3.506l-31.259 30.277Z" clip-rule="evenodd"/></g><defs><clipPath id="a">
<path class="tw-fill-primary-600" fill="#fff" d="M.09 0h143.82v112.705H.09z"/>
</clipPath>
</defs>
</svg>`;
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 100" fill="none">
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-width="2.477"
d="M104.552 42.242v12.023M48.415 17.31l6.51-4.858a7.43 7.43 0 0 1 8.984.074l1.172.904M24.844 34.897l-6.857 5.116A7.43 7.43 0 0 0 15 45.97v42.366a7.43 7.43 0 0 0 7.43 7.43h74.691a7.43 7.43 0 0 0 7.431-7.43V64.291" />
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-width="2.477"
d="M25.48 50.5V20.058a2.477 2.477 0 0 1 2.476-2.477h32.091" />
<path class="tw-stroke-art-accent" stroke-linecap="round" stroke-width="2.477"
d="M33.778 27.71h15.674M33.778 37.037h15.217M33.778 46.364h17.728M33.778 55.691h23.206" />
<path class="tw-stroke-art-primary" stroke-width="2.477"
d="M102.645 93.86 74.211 67.614a12.384 12.384 0 0 0-8.4-3.284H52.41c-3.216 0-6.306 1.251-8.616 3.488L16.904 93.86" />
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-width="2.477" d="m71.36 64.73 8.904-5.106" />
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-width="2.477" d="M1.238-1.238h37.47"
transform="matrix(.85856 .5127 .51278 -.85852 15.476 43.85)" />
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.477"
d="M104.722 34.286c0 13.922-11.286 25.207-25.209 25.207-13.922 0-25.208-11.285-25.208-25.207 0-13.92 11.286-25.206 25.208-25.206 13.923 0 25.209 11.285 25.209 25.206Z" />
<path class="tw-stroke-art-accent" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.238"
d="M101.346 34.287c0 11.972-9.82 21.678-21.932 21.678m0-43.355c-12.113 0-21.932 9.705-21.932 21.677" />
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.477"
d="m99.636 49.689 3.853 3.852 14.322 14.32a3.096 3.096 0 0 1 0 4.379l-.303.303a3.097 3.097 0 0 1-4.379 0l-14.322-14.32-3.852-3.853" />
</svg>
`;

View File

@ -1,20 +1,9 @@
import { svgIcon } from "@bitwarden/components";
export const RegistrationExpiredLinkIcon = svgIcon`
<svg width="64" height="64" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8198_6255)">
<path
class="tw-fill-primary-600"
d="M2.70609 5.38455C3.71495 3.18341 5.95991 1.65214 8.56633 1.65214C12.1167 1.65214 14.9948 4.4927 14.9948 7.99672C14.9948 11.5007 12.1167 14.3413 8.56633 14.3413C5.90396 14.3413 3.61848 12.7438 2.64241 10.4652C2.53502 10.2144 2.24202 10.0971 1.98799 10.2031C1.73396 10.3091 1.61509 10.5983 1.72248 10.849C2.8493 13.4796 5.48864 15.327 8.56633 15.327C12.6683 15.327 15.9936 12.0451 15.9936 7.99672C15.9936 3.9483 12.6683 0.666412 8.56633 0.666412C5.41721 0.666412 2.72675 2.60033 1.64603 5.33002L0.858226 3.92917C0.734524 3.70921 0.455929 3.63117 0.235967 3.75487C0.0160044 3.87858 -0.0620299 4.15717 0.0616722 4.37713L1.35147 6.6706C1.47517 6.89056 1.75377 6.9686 1.97373 6.8449L4.2672 5.5551C4.48716 5.4314 4.5652 5.1528 4.44149 4.93284C4.31779 4.71288 4.0392 4.63484 3.81923 4.75854L2.70609 5.38455Z"
fill="#175DDC" />
<path
class="tw-fill-primary-600"
d="M10.7225 10.2926C10.6408 10.3474 10.5437 10.3767 10.4447 10.3767C10.318 10.3767 10.1962 10.3283 10.1041 10.2418L8.14837 8.41125C8.09985 8.36578 8.06123 8.31053 8.03449 8.24991C8.00775 8.18928 7.99389 8.12376 7.99389 8.05727V3.49271C7.99389 3.36314 8.04588 3.23896 8.13896 3.14704C8.23204 3.05512 8.3578 3.00378 8.48901 3.00378C8.62021 3.00378 8.74597 3.05561 8.83905 3.14704C8.93213 3.23847 8.98412 3.36314 8.98412 3.49271V7.84605L10.7853 9.53284C10.8571 9.60031 10.9071 9.68734 10.9284 9.78268C10.9497 9.87802 10.9413 9.97776 10.9047 10.0687C10.868 10.1596 10.8042 10.2379 10.7225 10.2926Z"
fill="#175DDC" />
</g>
<defs>
<clipPath id="clip0_8198_6255">
<rect width="15.9952" height="15.9952" fill="white" />
</clipPath>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 100" fill="none">
<path class="tw-art-primary" fill="#175DDC"
d="M36.167 38.348c4.543-9.911 14.651-16.806 26.387-16.806 15.986 0 28.945 12.79 28.945 28.567 0 15.777-12.96 28.568-28.945 28.568-11.988 0-22.278-7.193-26.673-17.453-.484-1.13-1.803-1.657-2.947-1.18a2.209 2.209 0 0 0-1.195 2.908c5.073 11.845 16.957 20.163 30.815 20.163 18.47 0 33.442-14.777 33.442-33.006 0-18.228-14.972-33.006-33.442-33.006-14.18 0-26.294 8.708-31.16 20.999l-3.547-6.307a2.057 2.057 0 0 0-3.586 2.017l5.807 10.326a2.057 2.057 0 0 0 2.802.785l10.327-5.808a2.057 2.057 0 1 0-2.018-3.586l-5.012 2.819Z" />
<path class="tw-art-primary" fill="#175DDC"
d="M72.263 60.447a2.25 2.25 0 0 1-2.785-.229l-8.806-8.242a2.229 2.229 0 0 1-.512-.727 2.148 2.148 0 0 1-.183-.867V29.829c0-.583.234-1.142.653-1.556a2.251 2.251 0 0 1 3.152 0c.42.412.653.973.653 1.556v19.602l8.11 7.595a2.178 2.178 0 0 1 .537 2.413c-.164.41-.452.761-.82 1.008Z" />
</svg>`;

View File

@ -0,0 +1,41 @@
import { svgIcon } from "@bitwarden/components";
export const RegistrationLockAltIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 100" fill="none">
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M47.048 77.093a1 1 0 0 1 1 1v5.811a1 1 0 0 1-2 0v-5.811a1 1 0 0 1 1-1Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M53.547 81.8a1 1 0 0 1-.643 1.26l-5.548 1.795a1 1 0 1 1-.616-1.902l5.547-1.797a1 1 0 0 1 1.26.644Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M46.462 83.094a1 1 0 0 1 1.396.225l3.435 4.755a1 1 0 1 1-1.622 1.17l-3.434-4.754a1 1 0 0 1 .225-1.396Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M47.634 83.094a1 1 0 0 1 .225 1.396l-3.434 4.755a1 1 0 1 1-1.621-1.171l3.434-4.755a1 1 0 0 1 1.396-.225Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M40.549 81.8a1 1 0 0 1 1.26-.644l5.547 1.797a1 1 0 1 1-.617 1.902l-5.547-1.796a1 1 0 0 1-.643-1.26ZM65.91 77.093a1 1 0 0 1 1 1v5.811a1 1 0 0 1-2 0v-5.811a1 1 0 0 1 1-1Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M72.463 81.802a1 1 0 0 1-.647 1.258l-5.6 1.796a1 1 0 0 1-.611-1.904l5.6-1.796a1 1 0 0 1 1.258.646Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M65.325 83.094a1 1 0 0 1 1.396.225l3.434 4.755a1 1 0 0 1-1.621 1.17L65.1 84.49a1 1 0 0 1 .225-1.396Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M66.49 83.09a1 1 0 0 1 .235 1.394l-3.382 4.755a1 1 0 1 1-1.63-1.16l3.382-4.754a1 1 0 0 1 1.394-.236Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M59.41 81.8a1 1 0 0 1 1.26-.644l5.548 1.797a1 1 0 1 1-.616 1.902l-5.548-1.796a1 1 0 0 1-.643-1.26ZM77.5 88.5a1 1 0 0 1 1-1h11a1 1 0 1 1 0 2h-11a1 1 0 0 1-1-1ZM94.5 88.5a1 1 0 0 1 1-1h11a1 1 0 0 1 0 2h-11a1 1 0 0 1-1-1Z"
clip-rule="evenodd" />
<path class="tw-fill-art-accent" fill-rule="evenodd"
d="M105 72.5H46c-5.799 0-10.5 4.701-10.5 10.5v.5C35.5 89.299 40.201 94 46 94h59c5.799 0 10.5-4.701 10.5-10.5V83c0-5.799-4.701-10.5-10.5-10.5Zm-59-2c-6.904 0-12.5 5.596-12.5 12.5v.5C33.5 90.404 39.096 96 46 96h59c6.904 0 12.5-5.596 12.5-12.5V83c0-6.904-5.596-12.5-12.5-12.5H46Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M30.5 42.5a7 7 0 0 0-7 7V85a7 7 0 0 0 7 7h9v2h-9a9 9 0 0 1-9-9V49.5a9 9 0 0 1 9-9h49a9 9 0 0 1 9 9v22.25h-2V49.5a7 7 0 0 0-7-7h-49Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M54.75 5C43.592 5 34.5 14.056 34.5 25.15V41h-2V25.15C32.5 12.943 42.496 3 54.75 3 67.002 3 77 12.889 77 25.15V41h-2V25.15C75 14.005 65.91 5 54.75 5ZM51 54.158c0-1.996 1.543-3.658 3.5-3.658s3.5 1.662 3.5 3.658a3.722 3.722 0 0 1-1.201 2.758L56.8 57v6.106c0 1.298-1.006 2.395-2.3 2.395-1.294 0-2.3-1.097-2.3-2.395V57l.001-.083A3.722 3.722 0 0 1 51 54.158Zm3.5-2.658c-1.357 0-2.5 1.165-2.5 2.658a2.71 2.71 0 0 0 1.029 2.148l.231.18-.043.29c-.011.072-.017.147-.017.223v6.106c0 .795.606 1.395 1.3 1.395.694 0 1.3-.6 1.3-1.395V57c0-.076-.006-.15-.017-.224l-.043-.29.231-.179A2.71 2.71 0 0 0 57 54.158c0-1.493-1.143-2.658-2.5-2.658Z"
clip-rule="evenodd" />
</svg>`;

View File

@ -0,0 +1,24 @@
import { svgIcon } from "@bitwarden/components";
export const RegistrationUserAddIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 100" fill="none">
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M0 21.627a7.798 7.798 0 0 1 7.798-7.798H89.38a7.798 7.798 0 0 1 7.799 7.798v8.764h-2.4v-8.764a5.399 5.399 0 0 0-5.398-5.398H7.797A5.399 5.399 0 0 0 2.4 21.627v49.19a5.399 5.399 0 0 0 5.4 5.399h9.483v2.399H7.798A7.798 7.798 0 0 1 0 70.817v-49.19Zm49.378 54.589h13.498v2.399H49.378v-2.4Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M88.78 61.819c8.946 0 16.197-7.252 16.197-16.197s-7.251-16.196-16.196-16.196-16.197 7.251-16.197 16.196S79.836 61.82 88.781 61.82Zm0 2.4c10.27 0 18.597-8.326 18.597-18.597 0-10.27-8.326-18.596-18.596-18.596s-18.596 8.326-18.596 18.596S78.51 64.218 88.78 64.218Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd"
d="M61.005 90.612h54.28c1.303 0 1.833-.344 2.01-.53.128-.134.396-.517.226-1.586-2.187-13.74-14.213-24.278-28.752-24.278S62.203 74.757 60.017 88.496c-.09.569.026 1.226.279 1.662.115.199.23.305.316.359.072.045.184.095.393.095Zm0 2.4h54.28c3.346 0 5.104-1.76 4.605-4.894-2.371-14.903-15.402-26.3-31.121-26.3-15.72 0-28.75 11.397-31.122 26.3-.337 2.122.744 4.894 3.358 4.894Z"
clip-rule="evenodd" />
<path class="tw-fill-art-accent" fill-rule="evenodd"
d="M77.983 21.027a1.2 1.2 0 0 1-1.2-1.2v-.6a1.2 1.2 0 0 1 2.4 0v.6a1.2 1.2 0 0 1-1.2 1.2ZM83.382 21.027a1.2 1.2 0 0 1-1.2-1.2v-.497a1.2 1.2 0 1 1 2.4 0v.498a1.2 1.2 0 0 1-1.2 1.2ZM88.78 21.027a1.2 1.2 0 0 1-1.2-1.2v-.497a1.2 1.2 0 1 1 2.4 0v.498a1.2 1.2 0 0 1-1.2 1.2Z"
clip-rule="evenodd" />
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M95.083 24.027H0v-1.2h95.083v1.2Z"
clip-rule="evenodd" />
<g clip-path="url(#a)">
<path class="tw-fill-art-accent"
d="M32.727 93.11a16.908 16.908 0 0 1-9.477-2.91 17.228 17.228 0 0 1-6.284-7.752 17.467 17.467 0 0 1-.97-9.98 17.343 17.343 0 0 1 4.669-8.843 17.002 17.002 0 0 1 8.735-4.727 16.867 16.867 0 0 1 9.856.982 17.113 17.113 0 0 1 7.656 6.362 17.419 17.419 0 0 1 2.875 9.596 17.4 17.4 0 0 1-5.003 12.208 16.977 16.977 0 0 1-12.057 5.065Zm0-32.245c-2.924 0-5.784.877-8.216 2.523a14.932 14.932 0 0 0-5.447 6.72 15.148 15.148 0 0 0-.84 8.652 15.028 15.028 0 0 0 4.047 7.667 14.729 14.729 0 0 0 7.572 4.098c2.87.578 5.842.281 8.545-.851a14.827 14.827 0 0 0 6.637-5.515 15.11 15.11 0 0 0 2.493-8.319 15.087 15.087 0 0 0-4.336-10.583 14.714 14.714 0 0 0-10.453-4.39l-.002-.002Zm8.82 14.168h-6.9a1.105 1.105 0 0 1-.793-.333 1.149 1.149 0 0 1-.328-.802v-6.99a.81.81 0 0 0-.233-.568.793.793 0 0 0-1.357.569v6.985a1.144 1.144 0 0 1-.328.802 1.115 1.115 0 0 1-.793.332h-6.907a.788.788 0 0 0-.562.237.812.812 0 0 0 0 1.139.79.79 0 0 0 .562.235h6.899c.296 0 .582.12.791.333.21.212.329.5.329.8v6.99c0 .213.084.418.232.57a.788.788 0 0 0 1.125 0 .809.809 0 0 0 .232-.57v-6.98c0-.301.119-.59.329-.802.21-.212.494-.332.79-.332h6.906a.795.795 0 0 0 .795-.806.81.81 0 0 0-.232-.569.791.791 0 0 0-.563-.236l.004-.004h.002Z" />
</g>
</svg>
`;

View File

@ -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;

View File

@ -15,6 +15,10 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
protected accountApiService: AccountApiService,
) {}
getOrgNameFromOrgInvite(): Promise<string | null> {
return null;
}
getMasterPasswordPolicyOptsFromOrgInvite(): Promise<MasterPasswordPolicyOptions | null> {
return null;
}

View File

@ -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();

View File

@ -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,

View File

@ -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"

View File

@ -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>

View File

@ -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);
}

View File

@ -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: {