mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 09:13:33 +00:00
[AC-2156] Billing State Provider Migration (#8133)
* Added billing account profile state service
* Update usages after removing state service functions
* Added migrator
* Updated bw.ts and main.background.ts
* Removed comment
* Updated state service dependencies to include billing service
* Added missing mv3 factory and updated MainContextMenuHandler
* updated autofill service and tests
* Updated the remaining extensions usages
* Updated desktop
* Removed subjects where they weren't needed
* Refactored billing service to have a single setter to avoid unecessary emissions
* Refactored has premium guard to return an observable
* Renamed services to match ADR
f633f2cdd8/docs/architecture/clients/presentation/angular.md (abstract--default-implementations)
* Updated property names to be a smidgen more descriptive and added jsdocs
* Updated setting of canAccessPremium to automatically update when the underlying observable emits
* Fixed build error after merge conflicts
* Another build error from conflict
* Removed autofill unit test changes from conflict
* Updated login strategy to not set premium field using state service
* Updated CLI to use billing state provider
* Shortened names a bit
* Fixed build
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export type BillingAccountProfile = {
|
||||
hasPremiumPersonally: boolean;
|
||||
hasPremiumFromAnyOrganization: boolean;
|
||||
};
|
||||
|
||||
export abstract class BillingAccountProfileStateService {
|
||||
/**
|
||||
* Emits `true` when the active user's account has been granted premium from any of the
|
||||
* organizations it is a member of. Otherwise, emits `false`
|
||||
*/
|
||||
hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits `true` when the active user's account has an active premium subscription at the
|
||||
* individual user level
|
||||
*/
|
||||
hasPremiumPersonally$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits `true` when either `hasPremiumPersonally` or `hasPremiumFromAnyOrganization` is `true`
|
||||
*/
|
||||
hasPremiumFromAnySource$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Sets the active user's premium status fields upon every full sync, either from their personal
|
||||
* subscription to premium, or an organization they're a part of that grants them premium.
|
||||
* @param hasPremiumPersonally
|
||||
* @param hasPremiumFromAnyOrganization
|
||||
*/
|
||||
abstract setHasPremium(
|
||||
hasPremiumPersonally: boolean,
|
||||
hasPremiumFromAnyOrganization: boolean,
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeActiveUserStateProvider,
|
||||
mockAccountServiceWith,
|
||||
FakeActiveUserState,
|
||||
trackEmissions,
|
||||
} from "../../../../spec";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service";
|
||||
|
||||
import {
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
DefaultBillingAccountProfileStateService,
|
||||
} from "./billing-account-profile-state.service";
|
||||
|
||||
describe("BillingAccountProfileStateService", () => {
|
||||
let activeUserStateProvider: FakeActiveUserStateProvider;
|
||||
let sut: DefaultBillingAccountProfileStateService;
|
||||
let billingAccountProfileState: FakeActiveUserState<BillingAccountProfile>;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const userId = "fakeUserId" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
|
||||
|
||||
sut = new DefaultBillingAccountProfileStateService(activeUserStateProvider);
|
||||
|
||||
billingAccountProfileState = activeUserStateProvider.getFake(
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("accountHasPremiumFromAnyOrganization$", () => {
|
||||
it("should emit changes in hasPremiumFromAnyOrganization", async () => {
|
||||
billingAccountProfileState.nextState({
|
||||
hasPremiumPersonally: false,
|
||||
hasPremiumFromAnyOrganization: true,
|
||||
});
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit once when calling setHasPremium once", async () => {
|
||||
const emissions = trackEmissions(sut.hasPremiumFromAnyOrganization$);
|
||||
const startingEmissionCount = emissions.length;
|
||||
|
||||
await sut.setHasPremium(true, true);
|
||||
|
||||
const endingEmissionCount = emissions.length;
|
||||
expect(endingEmissionCount - startingEmissionCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasPremiumPersonally$", () => {
|
||||
it("should emit changes in hasPremiumPersonally", async () => {
|
||||
billingAccountProfileState.nextState({
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromAnyOrganization: false,
|
||||
});
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit once when calling setHasPremium once", async () => {
|
||||
const emissions = trackEmissions(sut.hasPremiumPersonally$);
|
||||
const startingEmissionCount = emissions.length;
|
||||
|
||||
await sut.setHasPremium(true, true);
|
||||
|
||||
const endingEmissionCount = emissions.length;
|
||||
expect(endingEmissionCount - startingEmissionCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canAccessPremium$", () => {
|
||||
it("should emit changes in hasPremiumPersonally", async () => {
|
||||
billingAccountProfileState.nextState({
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromAnyOrganization: false,
|
||||
});
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit changes in hasPremiumFromAnyOrganization", async () => {
|
||||
billingAccountProfileState.nextState({
|
||||
hasPremiumPersonally: false,
|
||||
hasPremiumFromAnyOrganization: true,
|
||||
});
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit changes in both hasPremiumPersonally and hasPremiumFromAnyOrganization", async () => {
|
||||
billingAccountProfileState.nextState({
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromAnyOrganization: true,
|
||||
});
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit once when calling setHasPremium once", async () => {
|
||||
const emissions = trackEmissions(sut.hasPremiumFromAnySource$);
|
||||
const startingEmissionCount = emissions.length;
|
||||
|
||||
await sut.setHasPremium(true, true);
|
||||
|
||||
const endingEmissionCount = emissions.length;
|
||||
expect(endingEmissionCount - startingEmissionCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setHasPremium", () => {
|
||||
it("should have `hasPremiumPersonally$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => {
|
||||
await sut.setHasPremium(true, false);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have `hasPremiumFromAnyOrganization$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => {
|
||||
await sut.setHasPremium(false, true);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have `hasPremiumPersonally$` emit `false` when passing `false` as an argument for hasPremiumPersonally", async () => {
|
||||
await sut.setHasPremium(false, false);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false);
|
||||
});
|
||||
|
||||
it("should have `hasPremiumFromAnyOrganization$` emit `false` when passing `false` as an argument for hasPremiumFromAnyOrganization", async () => {
|
||||
await sut.setHasPremium(false, false);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false);
|
||||
});
|
||||
|
||||
it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => {
|
||||
await sut.setHasPremium(true, false);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => {
|
||||
await sut.setHasPremium(false, true);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have `canAccessPremium$` emit `false` when passing `false` for all arguments", async () => {
|
||||
await sut.setHasPremium(false, false);
|
||||
|
||||
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
ActiveUserState,
|
||||
ActiveUserStateProvider,
|
||||
BILLING_DISK,
|
||||
KeyDefinition,
|
||||
} from "../../../platform/state";
|
||||
import {
|
||||
BillingAccountProfile,
|
||||
BillingAccountProfileStateService,
|
||||
} from "../../abstractions/account/billing-account-profile-state.service";
|
||||
|
||||
export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new KeyDefinition<BillingAccountProfile>(
|
||||
BILLING_DISK,
|
||||
"accountProfile",
|
||||
{
|
||||
deserializer: (billingAccountProfile) => billingAccountProfile,
|
||||
},
|
||||
);
|
||||
|
||||
export class DefaultBillingAccountProfileStateService implements BillingAccountProfileStateService {
|
||||
private billingAccountProfileState: ActiveUserState<BillingAccountProfile>;
|
||||
|
||||
hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
hasPremiumPersonally$: Observable<boolean>;
|
||||
hasPremiumFromAnySource$: Observable<boolean>;
|
||||
|
||||
constructor(activeUserStateProvider: ActiveUserStateProvider) {
|
||||
this.billingAccountProfileState = activeUserStateProvider.get(
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
);
|
||||
|
||||
this.hasPremiumFromAnyOrganization$ = this.billingAccountProfileState.state$.pipe(
|
||||
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization),
|
||||
);
|
||||
|
||||
this.hasPremiumPersonally$ = this.billingAccountProfileState.state$.pipe(
|
||||
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally),
|
||||
);
|
||||
|
||||
this.hasPremiumFromAnySource$ = this.billingAccountProfileState.state$.pipe(
|
||||
map(
|
||||
(billingAccountProfile) =>
|
||||
billingAccountProfile?.hasPremiumFromAnyOrganization ||
|
||||
billingAccountProfile?.hasPremiumPersonally,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async setHasPremium(
|
||||
hasPremiumPersonally: boolean,
|
||||
hasPremiumFromAnyOrganization: boolean,
|
||||
): Promise<void> {
|
||||
await this.billingAccountProfileState.update((billingAccountProfile) => {
|
||||
return {
|
||||
hasPremiumPersonally: hasPremiumPersonally,
|
||||
hasPremiumFromAnyOrganization: hasPremiumFromAnyOrganization,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -61,11 +61,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
|
||||
getHasPremiumPersonally: (options?: StorageOptions) => Promise<boolean>;
|
||||
setHasPremiumPersonally: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
setHasPremiumFromOrganization: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getHasPremiumFromOrganization: (options?: StorageOptions) => Promise<boolean>;
|
||||
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
|
||||
@@ -172,8 +172,6 @@ export class AccountProfile {
|
||||
emailVerified?: boolean;
|
||||
everBeenUnlocked?: boolean;
|
||||
forceSetPasswordReason?: ForceSetPasswordReason;
|
||||
hasPremiumPersonally?: boolean;
|
||||
hasPremiumFromOrganization?: boolean;
|
||||
lastSync?: string;
|
||||
userId?: string;
|
||||
usesKeyConnector?: boolean;
|
||||
|
||||
@@ -338,72 +338,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getCanAccessPremium(options?: StorageOptions): Promise<boolean> {
|
||||
if (!(await this.getIsAuthenticated(options))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(await this.getHasPremiumPersonally(options)) ||
|
||||
(await this.getHasPremiumFromOrganization(options))
|
||||
);
|
||||
}
|
||||
|
||||
async getHasPremiumPersonally(options?: StorageOptions): Promise<boolean> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
return account?.profile?.hasPremiumPersonally;
|
||||
}
|
||||
|
||||
async setHasPremiumPersonally(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.profile.hasPremiumPersonally = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getHasPremiumFromOrganization(options?: StorageOptions): Promise<boolean> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
|
||||
if (account.profile?.hasPremiumFromOrganization) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: older server versions won't send the hasPremiumFromOrganization flag, so we're keeping the old logic
|
||||
// for backwards compatibility. It can be removed after everyone has upgraded.
|
||||
const organizations = await this.getOrganizations(options);
|
||||
if (organizations == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const id of Object.keys(organizations)) {
|
||||
const o = organizations[id];
|
||||
if (o.enabled && o.usersGetPremium && !o.isProviderUser) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async setHasPremiumFromOrganization(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.profile.hasPremiumFromOrganization = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getConvertAccountToKeyConnector(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
|
||||
@@ -23,6 +23,9 @@ export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk");
|
||||
export const POLICIES_DISK = new StateDefinition("policies", "disk");
|
||||
export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
||||
|
||||
// Billing
|
||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
|
||||
// Auth
|
||||
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
@@ -43,15 +46,11 @@ export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
|
||||
"disk",
|
||||
);
|
||||
|
||||
// Billing
|
||||
|
||||
export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk");
|
||||
|
||||
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
|
||||
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
|
||||
// Components
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import { MoveThemeToStateProviderMigrator } from "./migrations/35-move-theme-to-
|
||||
import { VaultSettingsKeyMigrator } from "./migrations/36-move-show-card-and-identity-to-state-provider";
|
||||
import { AvatarColorMigrator } from "./migrations/37-move-avatar-color-to-state-providers";
|
||||
import { TokenServiceStateProviderMigrator } from "./migrations/38-migrate-token-svc-to-state-provider";
|
||||
import { MoveBillingAccountProfileMigrator } from "./migrations/39-move-billing-account-profile-to-state-providers";
|
||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
@@ -42,7 +43,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 38;
|
||||
export const CURRENT_VERSION = 39;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@@ -82,7 +83,8 @@ export function createMigrationBuilder() {
|
||||
.with(MoveThemeToStateProviderMigrator, 34, 35)
|
||||
.with(VaultSettingsKeyMigrator, 35, 36)
|
||||
.with(AvatarColorMigrator, 36, 37)
|
||||
.with(TokenServiceStateProviderMigrator, 37, CURRENT_VERSION);
|
||||
.with(TokenServiceStateProviderMigrator, 37, 38)
|
||||
.with(MoveBillingAccountProfileMigrator, 38, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { MockProxy, any } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
MoveBillingAccountProfileMigrator,
|
||||
} from "./39-move-billing-account-profile-to-state-providers";
|
||||
|
||||
const exampleJSON = () => ({
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromOrganization: false,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
});
|
||||
|
||||
const rollbackJSON = () => ({
|
||||
"user_user-1_billing_accountProfile": {
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromOrganization: false,
|
||||
},
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
});
|
||||
|
||||
describe("MoveBillingAccountProfileToStateProviders migrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: MoveBillingAccountProfileMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 39);
|
||||
sut = new MoveBillingAccountProfileMigrator(38, 39);
|
||||
});
|
||||
|
||||
it("removes from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets hasPremiumPersonally value for account that have it", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
{ hasPremiumFromOrganization: false, hasPremiumPersonally: true },
|
||||
);
|
||||
});
|
||||
|
||||
it("should not call extra setToUser", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 39);
|
||||
sut = new MoveBillingAccountProfileMigrator(38, 39);
|
||||
});
|
||||
|
||||
it("nulls out new values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
profile: {
|
||||
hasPremiumPersonally: true,
|
||||
hasPremiumFromOrganization: false,
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["user-2", "user-3"])(
|
||||
"does not restore values when accounts are not present",
|
||||
async (userId) => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith(userId, any());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
profile?: {
|
||||
hasPremiumPersonally?: boolean;
|
||||
hasPremiumFromOrganization?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedBillingAccountProfileType = {
|
||||
hasPremiumPersonally: boolean;
|
||||
hasPremiumFromOrganization: boolean;
|
||||
};
|
||||
|
||||
export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION: KeyDefinitionLike = {
|
||||
key: "accountProfile",
|
||||
stateDefinition: {
|
||||
name: "billing",
|
||||
},
|
||||
};
|
||||
|
||||
export class MoveBillingAccountProfileMigrator extends Migrator<38, 39> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
const migrateAccount = async (userId: string, account: ExpectedAccountType): Promise<void> => {
|
||||
const hasPremiumPersonally = account?.profile?.hasPremiumPersonally;
|
||||
const hasPremiumFromOrganization = account?.profile?.hasPremiumFromOrganization;
|
||||
|
||||
if (hasPremiumPersonally != null || hasPremiumFromOrganization != null) {
|
||||
await helper.setToUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, {
|
||||
hasPremiumPersonally: hasPremiumPersonally,
|
||||
hasPremiumFromOrganization: hasPremiumFromOrganization,
|
||||
});
|
||||
|
||||
delete account?.profile?.hasPremiumPersonally;
|
||||
delete account?.profile?.hasPremiumFromOrganization;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
const rollbackAccount = async (userId: string, account: ExpectedAccountType): Promise<void> => {
|
||||
const value = await helper.getFromUser<ExpectedBillingAccountProfileType>(
|
||||
userId,
|
||||
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
|
||||
);
|
||||
|
||||
if (account && value) {
|
||||
account.profile = Object.assign(account.profile ?? {}, {
|
||||
hasPremiumPersonally: value?.hasPremiumPersonally,
|
||||
hasPremiumFromOrganization: value?.hasPremiumFromOrganization,
|
||||
});
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, null);
|
||||
};
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { AvatarService } from "../../../auth/abstractions/avatar.service";
|
||||
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
|
||||
import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { DomainsResponse } from "../../../models/response/domains.response";
|
||||
import {
|
||||
SyncCipherNotification,
|
||||
@@ -62,6 +63,7 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
private sendApiService: SendApiService,
|
||||
private avatarService: AvatarService,
|
||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {}
|
||||
|
||||
async getLastSync(): Promise<Date> {
|
||||
@@ -314,8 +316,11 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
await this.avatarService.setAvatarColor(response.avatarColor);
|
||||
await this.stateService.setSecurityStamp(response.securityStamp);
|
||||
await this.stateService.setEmailVerified(response.emailVerified);
|
||||
await this.stateService.setHasPremiumPersonally(response.premiumPersonally);
|
||||
await this.stateService.setHasPremiumFromOrganization(response.premiumFromOrganization);
|
||||
|
||||
await this.billingAccountProfileStateService.setHasPremium(
|
||||
response.premiumPersonally,
|
||||
response.premiumFromOrganization,
|
||||
);
|
||||
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
|
||||
|
||||
await this.setForceSetPasswordReasonIfNeeded(response);
|
||||
|
||||
Reference in New Issue
Block a user