diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index d390f5c360..239ac835f5 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -4,6 +4,11 @@ import { firstValueFrom } from "rxjs"; import { BaseAcceptComponent } from "../../../common/base.accept.component"; +/* + * This component is responsible for handling the acceptance of a families plan sponsorship invite. + * "Bitwarden allows all members of Enterprise Organizations to redeem a complimentary Families Plan with their + * personal email address." - https://bitwarden.com/learning/free-families-plan-for-enterprise/ + */ @Component({ selector: "app-accept-family-sponsorship", templateUrl: "accept-family-sponsorship.component.html", @@ -26,9 +31,32 @@ export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/login"], { queryParams: { email: qParams.email } }); } else { - // TODO: remove when email verification flag is removed - const registerRoute = await firstValueFrom(this.registerRoute$); - await this.router.navigate([registerRoute], { queryParams: { email: qParams.email } }); + // TODO: update logic when email verification flag is removed + let queryParams: Params; + let registerRoute = await firstValueFrom(this.registerRoute$); + if (registerRoute === "/register") { + queryParams = { + email: qParams.email, + }; + } else if (registerRoute === "/signup") { + // We have to override the base component route as we don't need users to + // complete email verification if they are coming directly an emailed invite. + + // TODO: in the future, to allow users to enter a name, consider sending all invite users to + // start registration page with prefilled email and a named token to be passed directly + // along to the finish-signup page without requiring email verification as + // we can treat the existence of the token as a form of email verification. + + registerRoute = "/finish-signup"; + queryParams = { + email: qParams.email, + orgSponsoredFreeFamilyPlanToken: qParams.token, + }; + } + + await this.router.navigate([registerRoute], { + queryParams: queryParams, + }); } } } diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index b54fd79a94..cf54c5a535 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -66,17 +66,18 @@ export class WebRegistrationFinishService // Note: the org invite token and email verification are mutually exclusive. Only one will be present. override async buildRegisterRequest( email: string, - emailVerificationToken: string, passwordInputResult: PasswordInputResult, encryptedUserKey: EncryptedString, userAsymmetricKeys: [string, EncString], + emailVerificationToken?: string, + orgSponsoredFreeFamilyPlanToken?: string, ): Promise { const registerRequest = await super.buildRegisterRequest( email, - emailVerificationToken, passwordInputResult, encryptedUserKey, userAsymmetricKeys, + emailVerificationToken, ); // web specific logic @@ -89,6 +90,10 @@ export class WebRegistrationFinishService } // Invite is accepted after login (on deep link redirect). + if (orgSponsoredFreeFamilyPlanToken) { + registerRequest.orgSponsoredFreeFamilyPlanToken = orgSponsoredFreeFamilyPlanToken; + } + return registerRequest; } } diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index 6013688df2..82f24974e2 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -99,8 +99,7 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { email: invite.email, }; } else if (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 + // We have to override the base component route as we don't need users to complete email verification // if they are coming directly from an emailed org invite. registerRoute = "/finish-signup"; queryParams = { diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 85faf87144..882dd7b76c 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -23,6 +23,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi email: string, passwordInputResult: PasswordInputResult, emailVerificationToken?: string, + orgSponsoredFreeFamilyPlanToken?: string, ): Promise { const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey( passwordInputResult.masterKey, @@ -35,10 +36,11 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi const registerRequest = await this.buildRegisterRequest( email, - emailVerificationToken, passwordInputResult, newEncUserKey.encryptedString, userAsymmetricKeys, + emailVerificationToken, + orgSponsoredFreeFamilyPlanToken, ); const capchaBypassToken = await this.accountApiService.registerFinish(registerRequest); @@ -48,19 +50,19 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi protected async buildRegisterRequest( email: string, - emailVerificationToken: string, passwordInputResult: PasswordInputResult, encryptedUserKey: EncryptedString, userAsymmetricKeys: [string, EncString], + emailVerificationToken?: string, + orgSponsoredFreeFamilyPlanToken?: string, // web only ): Promise { const userAsymmetricKeysRequest = new KeysRequest( userAsymmetricKeys[0], userAsymmetricKeys[1].encryptedString, ); - return new RegisterFinishRequest( + const registerFinishRequest = new RegisterFinishRequest( email, - emailVerificationToken, passwordInputResult.masterKeyHash, passwordInputResult.hint, encryptedUserKey, @@ -68,5 +70,11 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi passwordInputResult.kdfConfig.kdfType, passwordInputResult.kdfConfig.iterations, ); + + if (emailVerificationToken) { + registerFinishRequest.emailVerificationToken = emailVerificationToken; + } + + return registerFinishRequest; } } diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 3c7e31b5de..e9703c6085 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -33,11 +33,17 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { submitting = false; email: string; - // Note: this token is the email verification token. It is always supplied as a query param, but + // Note: this token is the email verification token. When it is supplied as a query param, // it either comes from the email verification email or, if email verification is disabled server side // via global settings, it comes directly from the registration-start component directly. + // It is not provided when the user is coming from another emailed invite (ex: org invite or enterprise + // org sponsored free family plan invite). emailVerificationToken: string; + // this token is provided when the user is coming from an emailed invite to + // setup a free family plan sponsored by an organization but they don't have an account yet. + orgSponsoredFreeFamilyPlanToken: string; + masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null; constructor( @@ -69,6 +75,10 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { if (qParams.token != null) { this.emailVerificationToken = qParams.token; } + + if (qParams.orgSponsoredFreeFamilyPlanToken != null) { + this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken; + } }), switchMap((qParams: Params) => { if ( @@ -100,6 +110,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.email, passwordInputResult, this.emailVerificationToken, + this.orgSponsoredFreeFamilyPlanToken, ); } catch (e) { this.validationService.showError(e); diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index 63e23182f6..0c674d566c 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -14,12 +14,14 @@ export abstract class RegistrationFinishService { * * @param email The email address of the user. * @param passwordInputResult The password input result. - * @param emailVerificationToken The optional email verification token. Not present in org invite scenarios. + * @param emailVerificationToken The optional email verification token. Not present in emailed invite scenarios (ex: org invite). + * @param orgSponsoredFreeFamilyPlanToken The optional org sponsored free family plan token. * Returns a promise which resolves to the captcha bypass token string upon a successful account creation. */ abstract finishRegistration( email: string, passwordInputResult: PasswordInputResult, emailVerificationToken?: string, + orgSponsoredFreeFamilyPlanToken?: string, ): Promise; } diff --git a/libs/common/src/auth/models/request/registration/register-finish.request.ts b/libs/common/src/auth/models/request/registration/register-finish.request.ts index 22275fb228..5e412aa4e6 100644 --- a/libs/common/src/auth/models/request/registration/register-finish.request.ts +++ b/libs/common/src/auth/models/request/registration/register-finish.request.ts @@ -5,7 +5,6 @@ import { EncryptedString } from "../../../../platform/models/domain/enc-string"; export class RegisterFinishRequest { constructor( public email: string, - public emailVerificationToken: string, public masterPasswordHash: string, public masterPasswordHint: string, @@ -18,6 +17,9 @@ export class RegisterFinishRequest { public kdfMemory?: number, public kdfParallelism?: number, + public emailVerificationToken?: string, + public orgSponsoredFreeFamilyPlanToken?: string, + // Org Invite data (only applies on web) public organizationUserId?: string, public orgInviteToken?: string,