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 a9eba08be8c..b5624f4f247 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 @@ -20,6 +20,9 @@ import { } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { KeyService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/user-core"; + +import { SetupPremiumService } from "../../../../billing/individual/premium/setup-premium.service"; export class WebRegistrationFinishService extends DefaultRegistrationFinishService @@ -32,6 +35,7 @@ export class WebRegistrationFinishService private policyApiService: PolicyApiServiceAbstraction, private logService: LogService, private policyService: PolicyService, + private setupPremiumService: SetupPremiumService, ) { super(keyService, accountApiService); } @@ -123,4 +127,8 @@ export class WebRegistrationFinishService return registerRequest; } + + async establishIntentToSetupPremium(userId: UserId) { + await this.setupPremiumService.setIntentToSetupPremium(true, userId); + } } diff --git a/apps/web/src/app/billing/individual/premium/setup-premium.service.ts b/apps/web/src/app/billing/individual/premium/setup-premium.service.ts new file mode 100644 index 00000000000..ea3fdc6d080 --- /dev/null +++ b/apps/web/src/app/billing/individual/premium/setup-premium.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { BILLING_DISK_LOCAL, StateProvider, UserKeyDefinition } from "@bitwarden/state"; +import { UserId } from "@bitwarden/user-core"; + +export const INTENT_TO_SETUP_PREMIUM_KEY = new UserKeyDefinition( + BILLING_DISK_LOCAL, + "intentToSetupPremium", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +@Injectable({ + providedIn: "root", +}) +export class SetupPremiumService { + constructor(private stateProvider: StateProvider) {} + + async setIntentToSetupPremium(intent: boolean, userId: UserId) { + await this.stateProvider.setUserState(INTENT_TO_SETUP_PREMIUM_KEY, intent, userId); + } + + async getIntentToSetupPremium(userId: UserId) { + await firstValueFrom(this.stateProvider.getUserState$(INTENT_TO_SETUP_PREMIUM_KEY, userId)); + } + + async clearIntentToSetupPremium(userId: UserId) { + await this.stateProvider.setUserState(INTENT_TO_SETUP_PREMIUM_KEY, null, userId); + } +} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 29b84ddc382..6a7858e3119 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -127,6 +127,7 @@ import { WebSetInitialPasswordService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; +import { SetupPremiumService } from "../billing/individual/premium/setup-premium.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; import { WebFileDownloadService } from "../core/web-file-download.service"; @@ -266,6 +267,7 @@ const safeProviders: SafeProvider[] = [ PolicyApiServiceAbstraction, LogService, PolicyService, + SetupPremiumService, ], }), safeProvider({ 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 2bef5670ac3..0a30dcf3ed3 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 @@ -9,6 +9,7 @@ import { } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { KeyService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/user-core"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -96,4 +97,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi return registerFinishRequest; } + + async establishIntentToSetupPremium(userId: UserId) { + // no-op + } } 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 7e7b9131fac..18623ea692e 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 @@ -67,6 +67,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null; + intendsToSetupPremium = false; + constructor( private activatedRoute: ActivatedRoute, private router: Router, @@ -126,6 +128,25 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.providerInviteToken = qParams.providerInviteToken; this.providerUserId = qParams.providerUserId; } + + // -- Proof of Concept (start) -- + const mockParams = { + fromMarketing: "premium", // `&fromMarketing=premium` + }; + + if (mockParams.fromMarketing && mockParams.fromMarketing === "premium") { + this.intendsToSetupPremium = true; + } + + // Note: To avoid magic strings, we can create an enum-like type and use it above: + // ``` + // export const MarketingInitiative = Object.freeze({ + // Premium: "premium", + // Families: "families", + // // Other variants in the future + // } as const); + + // -- Proof of Concept (end) -- } private async initOrgInviteFlowIfPresent(): Promise { @@ -190,6 +211,12 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { await this.loginSuccessHandlerService.run(authenticationResult.userId); + if (this.intendsToSetupPremium) { + await this.registrationFinishService.establishIntentToSetupPremium( + authenticationResult.userId, + ); + } + await this.router.navigate(["/vault"]); } catch (e) { // If login errors, redirect to login page per product. Don't show error 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 5f3c04e5155..559a878c6cc 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 @@ -1,4 +1,5 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { UserId } from "@bitwarden/user-core"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -39,4 +40,6 @@ export abstract class RegistrationFinishService { providerInviteToken?: string, providerUserId?: string, ): Promise; + + abstract establishIntentToSetupPremium(userId: UserId): Promise; } diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index 1c72f5f3230..eabe066722a 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -40,6 +40,7 @@ export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk"); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk"); +export const BILLING_DISK_LOCAL = new StateDefinition("billing", "disk", { web: "disk-local" }); // Auth