1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

Merge branch 'main' into PM-9022-scaffold-the-extension-and-build-pipeline

This commit is contained in:
Andreas Coroiu
2024-07-04 11:02:09 +02:00
committed by GitHub
34 changed files with 791 additions and 69 deletions

View File

@@ -8,7 +8,6 @@ import {
AuthRequestServiceAbstraction,
AuthRequestService,
LoginEmailServiceAbstraction,
LoginEmailService,
LogoutReason,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
@@ -708,8 +707,6 @@ export default class MainBackground {
this.stateProvider,
);
this.loginEmailService = new LoginEmailService(this.stateProvider);
this.ssoLoginService = new SsoLoginService(this.stateProvider);
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
@@ -1255,9 +1252,6 @@ export default class MainBackground {
clearCaches();
if (userId == null) {
this.loginEmailService.setRememberEmail(false);
await this.loginEmailService.saveEmailSettings();
await this.refreshBadge();
await this.refreshMenu();
await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts

View File

@@ -14,6 +14,8 @@ switch (process.platform) {
default:
targets = ['x86_64-unknown-linux-musl'];
process.env["PKG_CONFIG_ALLOW_CROSS"] = "1";
process.env["PKG_CONFIG_ALL_STATIC"] = "1";
break;
}

View File

@@ -24,7 +24,7 @@
"**/node_modules/argon2/package.json",
"**/node_modules/argon2/build/Release/argon2.node"
],
"electronVersion": "30.1.2",
"electronVersion": "31.1.0",
"generateUpdatesFilesForAllChannels": true,
"publish": {
"provider": "generic",

View File

@@ -165,13 +165,10 @@ export class AccountSwitcherComponent {
async addAccount() {
this.close();
this.loginEmailService.setRememberEmail(false);
await this.loginEmailService.saveEmailSettings();
await this.router.navigate(["/login"]);
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.stateService.clearDecryptedData(activeAccount?.id as UserId);
await this.accountService.switchAccount(null);
await this.router.navigate(["/login"]);
}
private async createInactiveAccounts(baseAccounts: {

View File

@@ -291,8 +291,7 @@ export class Main {
});
},
(e: any) => {
// eslint-disable-next-line
console.error(e);
this.logService.error("Error while running migrations:", e);
},
);
}

View File

@@ -82,9 +82,14 @@ export class ElectronStorageService implements AbstractStorageService {
}
save(key: string, obj: unknown): Promise<void> {
if (obj === undefined) {
return this.remove(key);
}
if (obj instanceof Set) {
obj = Array.from(obj);
}
this.store.set(key, obj);
this.updatesSubject.next({ key, updateType: "save" });
return Promise.resolve();

View File

@@ -71,6 +71,11 @@ type GroupDetailsRow = {
collectionNames?: string[];
};
/**
* @deprecated To be replaced with NewGroupsComponent which significantly refactors this component.
* The GroupsComponentRefactor flag switches between the old and new components; this component will be removed when
* the feature flag is removed.
*/
@Component({
selector: "app-org-groups",
templateUrl: "groups.component.html",

View File

@@ -0,0 +1,109 @@
<app-header>
<bit-search
[placeholder]="'searchGroups' | i18n"
[formControl]="searchControl"
class="tw-w-80"
></bit-search>
<button bitButton type="button" buttonType="primary" (click)="add()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newGroup" | i18n }}
</button>
</app-header>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="!loading">
<p *ngIf="!dataSource.filteredData.length">{{ "noGroupsInList" | i18n }}</p>
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
from overflowing the <main> element. -->
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
<bit-table *ngIf="dataSource.filteredData.length" [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell class="tw-w-20">
<input
type="checkbox"
bitCheckbox
class="tw-mr-2"
(change)="toggleAllVisible($event)"
id="selectAll"
/>
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
"all" | i18n
}}</label>
</th>
<th bitCell bitSortable="name" default>{{ "name" | i18n }}</th>
<th bitCell>{{ "collections" | i18n }}</th>
<th bitCell class="tw-w-10">
<button
[bitMenuTriggerFor]="headerMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
<button type="button" bitMenuItem (click)="deleteAllSelected()">
<span class="tw-text-danger"
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
>
</button>
</bit-menu>
</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *cdkVirtualFor="let g of rows$" [ngClass]="rowHeightClass">
<td bitCell (click)="check(g)" class="tw-cursor-pointer">
<input type="checkbox" bitCheckbox [(ngModel)]="g.checked" />
</td>
<td bitCell class="tw-cursor-pointer tw-font-bold" (click)="edit(g)">
<button type="button" bitLink>
{{ g.details.name }}
</button>
</td>
<td bitCell (click)="edit(g, ModalTabType.Collections)" class="tw-cursor-pointer">
<bit-badge-list
[items]="g.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="edit(g)">
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "editInfo" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Members)">
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "members" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Collections)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
</button>
<button type="button" bitMenuItem (click)="delete(g)">
<span class="tw-text-danger"
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</cdk-virtual-scroll-viewport>
</ng-container>

View File

@@ -0,0 +1,255 @@
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import {
BehaviorSubject,
combineLatest,
concatMap,
from,
lastValueFrom,
map,
switchMap,
tap,
} from "rxjs";
import { debounceTime, first } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
import {
CollectionDetailsResponse,
CollectionResponse,
} from "@bitwarden/common/vault/models/response/collection.response";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
import { InternalGroupService as GroupService, GroupView } from "../core";
import {
GroupAddEditDialogResultType,
GroupAddEditTabType,
openGroupAddEditDialog,
} from "./group-add-edit.component";
type GroupDetailsRow = {
/**
* Details used for displaying group information
*/
details: GroupView;
/**
* True if the group is selected in the table
*/
checked?: boolean;
/**
* A list of collection names the group has access to
*/
collectionNames?: string[];
};
/**
* Custom filter predicate that filters the groups table by id and name only.
* This is required because the default implementation searches by all properties, which can unintentionally match
* with members' names (who are assigned to the group) or collection names (which the group has access to).
*/
const groupsFilter = (filter: string) => {
const transformedFilter = filter.trim().toLowerCase();
return (data: GroupDetailsRow) => {
const group = data.details;
return (
group.id.toLowerCase().indexOf(transformedFilter) != -1 ||
group.name.toLowerCase().indexOf(transformedFilter) != -1
);
};
};
@Component({
templateUrl: "new-groups.component.html",
})
export class NewGroupsComponent {
loading = true;
organizationId: string;
protected dataSource = new TableDataSource<GroupDetailsRow>();
protected searchControl = new FormControl("");
// Fixed sizes used for cdkVirtualScroll
protected rowHeight = 46;
protected rowHeightClass = `tw-h-[46px]`;
protected ModalTabType = GroupAddEditTabType;
private refreshGroups$ = new BehaviorSubject<void>(null);
constructor(
private apiService: ApiService,
private groupService: GroupService,
private route: ActivatedRoute,
private i18nService: I18nService,
private dialogService: DialogService,
private logService: LogService,
private collectionService: CollectionService,
private toastService: ToastService,
) {
this.route.params
.pipe(
tap((params) => (this.organizationId = params.organizationId)),
switchMap(() =>
combineLatest([
// collectionMap
from(this.apiService.getCollections(this.organizationId)).pipe(
concatMap((response) => this.toCollectionMap(response)),
),
// groups
this.refreshGroups$.pipe(
switchMap(() => this.groupService.getAll(this.organizationId)),
),
]),
),
map(([collectionMap, groups]) => {
return groups.map<GroupDetailsRow>((g) => ({
id: g.id,
name: g.name,
details: g,
checked: false,
collectionNames: g.collections
.map((c) => collectionMap[c.id]?.name)
.sort(this.i18nService.collator?.compare),
}));
}),
takeUntilDestroyed(),
)
.subscribe((groups) => {
this.dataSource.data = groups;
this.loading = false;
});
// Connect the search input to the table dataSource filter input
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = groupsFilter(v)));
this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => {
this.searchControl.setValue(qParams.search);
});
}
async edit(
group: GroupDetailsRow,
startingTabIndex: GroupAddEditTabType = GroupAddEditTabType.Info,
) {
const dialogRef = openGroupAddEditDialog(this.dialogService, {
data: {
initialTab: startingTabIndex,
organizationId: this.organizationId,
groupId: group != null ? group.details.id : null,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result == GroupAddEditDialogResultType.Saved) {
this.refreshGroups$.next();
} else if (result == GroupAddEditDialogResultType.Deleted) {
this.removeGroup(group);
}
}
async add() {
await this.edit(null);
}
async delete(groupRow: GroupDetailsRow) {
const confirmed = await this.dialogService.openSimpleDialog({
title: groupRow.details.name,
content: { key: "deleteGroupConfirmation" },
type: "warning",
});
if (!confirmed) {
return false;
}
try {
await this.groupService.delete(this.organizationId, groupRow.details.id);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedGroupId", groupRow.details.name),
});
this.removeGroup(groupRow);
} catch (e) {
this.logService.error(e);
}
}
async deleteAllSelected() {
const groupsToDelete = this.dataSource.data.filter((g) => g.checked);
if (groupsToDelete.length == 0) {
return;
}
const deleteMessage = groupsToDelete.map((g) => g.details.name).join(", ");
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteMultipleGroupsConfirmation",
placeholders: [groupsToDelete.length.toString()],
},
content: deleteMessage,
type: "warning",
});
if (!confirmed) {
return false;
}
try {
await this.groupService.deleteMany(
this.organizationId,
groupsToDelete.map((g) => g.details.id),
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedManyGroups", groupsToDelete.length.toString()),
});
groupsToDelete.forEach((g) => this.removeGroup(g));
} catch (e) {
this.logService.error(e);
}
}
check(groupRow: GroupDetailsRow) {
groupRow.checked = !groupRow.checked;
}
toggleAllVisible(event: Event) {
this.dataSource.filteredData.forEach(
(g) => (g.checked = (event.target as HTMLInputElement).checked),
);
}
private removeGroup(groupRow: GroupDetailsRow) {
// Assign a new array to dataSource.data to trigger the setters and update the table
this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow);
}
private async toCollectionMap(response: ListResponse<CollectionResponse>) {
const collections = response.data.map(
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
);
const decryptedCollections = await this.collectionService.decryptMany(collections);
// Convert to an object using collection Ids as keys for faster name lookups
const collectionMap: Record<string, CollectionView> = {};
decryptedCollections.forEach((c) => (collectionMap[c.id] = c));
return collectionMap;
}
}

View File

@@ -2,6 +2,7 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import {
canAccessOrgAdmin,
canAccessGroupsTab,
@@ -11,11 +12,13 @@ import {
canAccessSettingsTab,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component";
import { GroupsComponent } from "../../admin-console/organizations/manage/groups.component";
import { NewGroupsComponent } from "../../admin-console/organizations/manage/new-groups.component";
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
import { VaultModule } from "../../vault/org-vault/vault.module";
@@ -46,14 +49,18 @@ const routes: Routes = [
path: "members",
loadChildren: () => import("./members").then((m) => m.MembersModule),
},
{
path: "groups",
component: GroupsComponent,
canActivate: [organizationPermissionsGuard(canAccessGroupsTab)],
data: {
titleId: "groups",
...featureFlaggedRoute({
defaultComponent: GroupsComponent,
flaggedComponent: NewGroupsComponent,
featureFlag: FeatureFlag.GroupsComponentRefactor,
routeOptions: {
path: "groups",
canActivate: [organizationPermissionsGuard(canAccessGroupsTab)],
data: {
titleId: "groups",
},
},
},
}),
{
path: "reporting",
loadChildren: () =>

View File

@@ -1,3 +1,4 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "../../shared";
@@ -5,6 +6,7 @@ import { LooseComponentsModule } from "../../shared";
import { CoreOrganizationModule } from "./core";
import { GroupAddEditComponent } from "./manage/group-add-edit.component";
import { GroupsComponent } from "./manage/groups.component";
import { NewGroupsComponent } from "./manage/new-groups.component";
import { OrganizationsRoutingModule } from "./organization-routing.module";
import { SharedOrganizationModule } from "./shared";
import { AccessSelectorModule } from "./shared/components/access-selector";
@@ -16,7 +18,8 @@ import { AccessSelectorModule } from "./shared/components/access-selector";
CoreOrganizationModule,
OrganizationsRoutingModule,
LooseComponentsModule,
ScrollingModule,
],
declarations: [GroupsComponent, GroupAddEditComponent],
declarations: [GroupsComponent, NewGroupsComponent, GroupAddEditComponent],
})
export class OrganizationModule {}

View File

@@ -22,11 +22,21 @@ const routes: Routes = [
canActivate: [organizationRedirectGuard(getSettingsRoute)],
children: [], // This is required to make the auto redirect work,
},
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
{
path: "account",
component: AccountComponent,
canActivate: [organizationPermissionsGuard((o) => o.isOwner)],
data: {
titleId: "organizationInfo",
},
},
{
path: "two-factor",
component: TwoFactorSetupComponent,
data: { titleId: "twoStepLogin" },
canActivate: [organizationPermissionsGuard((o) => o.use2fa && o.isOwner)],
data: {
titleId: "twoStepLogin",
},
},
{
path: "policies",

View File

@@ -8511,14 +8511,18 @@
"downloadCSV": {
"message": "Download CSV"
},
"billingHistoryDescription": {
"message": "Download a CSV to obtain client details for each billing date. Prorated charges are not included in the CSV and may vary from the linked invoice. For the most accurate billing details, refer to your monthly invoices.",
"description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations."
},
"monthlySubscriptionUserSeatsMessage": {
"message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. "
},
"annualSubscriptionUserSeatsMessage": {
"message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. "
},
"billingHistoryDescription": {
"message": "Download a CSV to obtain client details for each billing date. Prorated charges are not included in the CSV and may vary from the linked invoice. For the most accurate billing details, refer to your monthly invoices.",
"description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations."
},
"noInvoicesToList": {
"message": "There are no invoices to list",
"description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations."
}
}

View File

@@ -19,8 +19,8 @@ import {
ProviderPaymentMethodComponent,
ProviderSelectPaymentMethodDialogComponent,
ProviderSubscriptionComponent,
ProviderSubscriptionStatusComponent,
} from "../../billing/providers";
import { ProviderSubscriptionStatusComponent } from "../../billing/providers/subscription/provider-subscription-status.component";
import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component";

View File

@@ -17,6 +17,7 @@
*ngFor="let planCard of planCards"
[ngClass]="getPlanCardContainerClasses(planCard.selected)"
(click)="selectPlan(planCard.name)"
tabindex="0"
>
<div class="tw-relative">
<div
@@ -25,12 +26,12 @@
>
{{ "selected" | i18n }}
</div>
<div class="tw-p-5" [ngClass]="{ 'tw-pt-12': !planCard.selected }">
<div class="tw-pl-5 tw-py-4 tw-pr-4" [ngClass]="{ 'tw-pt-10': !planCard.selected }">
<h3 class="tw-text-2xl tw-font-bold tw-uppercase">{{ planCard.name }}</h3>
<span class="tw-text-2xl tw-font-semibold">{{
planCard.cost | currency: "$"
}}</span>
<span class="tw-text-sm tw-font-bold">/{{ "monthPerMember" | i18n }}</span>
<span class="tw-text-sm tw-font-bold">/ {{ "monthPerMember" | i18n }}</span>
</div>
</div>
</div>

View File

@@ -28,8 +28,8 @@
<tr>
<th colspan="2" bitCell bitSortable="organizationName" default>{{ "client" | i18n }}</th>
<th bitCell bitSortable="seats">{{ "assigned" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "used" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "remaining" | i18n }}</th>
<th bitCell bitSortable="occupiedSeats">{{ "used" | i18n }}</th>
<th bitCell bitSortable="remainingSeats">{{ "remaining" | i18n }}</th>
<th bitCell bitSortable="plan">{{ "billingPlan" | i18n }}</th>
<th></th>
</tr>
@@ -47,18 +47,18 @@
</div>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats }}</span>
<span class="tw-text-muted">{{ client.seats }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.userCount }}</span>
<span class="tw-text-muted">{{ client.assignedSeats }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats - client.userCount }}</span>
<span class="tw-text-muted">{{ client.remainingSeats }}</span>
</td>
<td>
<span>{{ client.plan }}</span>
<td bitCell class="tw-whitespace-nowrap">
<span class="tw-text-muted">{{ removeMonthly(client.plan) }}</span>
</td>
<td bitCell>
<td bitCell class="tw-text-right">
<button
[bitMenuTriggerFor]="rowMenu"
type="button"

View File

@@ -99,6 +99,8 @@ export class ManageClientsComponent extends BaseClientsComponent {
super.ngOnDestroy();
}
removeMonthly = (plan: string) => plan.replace(" (Monthly)", "");
async load() {
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));

View File

@@ -4,3 +4,4 @@ export * from "./guards/has-consolidated-billing.guard";
export * from "./payment-method/provider-select-payment-method-dialog.component";
export * from "./payment-method/provider-payment-method.component";
export * from "./subscription/provider-subscription.component";
export * from "./subscription/provider-subscription-status.component";

View File

@@ -126,7 +126,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
let rememberEmail = this.loginEmailService.getRememberEmail();
if (rememberEmail == null) {
if (!rememberEmail) {
rememberEmail = (await firstValueFrom(this.loginEmailService.storedEmail$)) != null;
}

View File

@@ -1,5 +1,6 @@
export * from "./add-account-credit-dialog/add-account-credit-dialog.component";
export * from "./invoices/invoices.component";
export * from "./invoices/no-invoices.component";
export * from "./manage-tax-information/manage-tax-information.component";
export * from "./select-payment-method/select-payment-method.component";
export * from "./verify-bank-account/verify-bank-account.component";

View File

@@ -55,3 +55,6 @@
</tr>
</ng-template>
</bit-table>
<div *ngIf="!invoices || invoices.length === 0" class="tw-mt-10">
<app-no-invoices></app-no-invoices>
</div>

View File

@@ -0,0 +1,36 @@
import { Component } from "@angular/core";
import { svgIcon } from "@bitwarden/components";
const partnerTrustIcon = svgIcon`
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M69 43.6511C70.1025 44.1328 71.32 44.4 72.6 44.4C77.5706 44.4 81.6 40.3706 81.6 35.4C81.6 30.4294 77.5706 26.4 72.6 26.4C67.6295 26.4 63.6 30.4294 63.6 35.4C63.6 36.0164 63.662 36.6184 63.7801 37.2" stroke="#CED4DC" stroke-width="2"/>
<path d="M69 44.8416C70.1522 44.553 71.3559 44.4 72.5943 44.4C80.1533 44.4 86.4194 50.0998 87.5598 57.5529C87.7996 59.1204 86.9541 60 85.3451 60C79.7231 60 75.6474 60 71.4 60" stroke="#CED4DC" stroke-width="2"/>
<path d="M51 43.6511C49.8976 44.1328 48.68 44.4 47.4 44.4C42.4295 44.4 38.4 40.3706 38.4 35.4C38.4 30.4294 42.4295 26.4 47.4 26.4C52.3706 26.4 56.4 30.4294 56.4 35.4C56.4 36.0164 56.338 36.6184 56.22 37.2" stroke="#CED4DC" stroke-width="2"/>
<path d="M51 44.8416C49.8478 44.553 48.6441 44.4 47.4057 44.4C39.8467 44.4 33.5806 50.0998 32.4402 57.5529C32.2004 59.1204 33.0459 60 34.6549 60C40.2769 60 44.3526 60 48.6 60" stroke="#CED4DC" stroke-width="2"/>
<circle cx="60" cy="45.6" r="9" stroke="#CED4DC" stroke-width="2"/>
<path d="M72.7451 70.2C62.3773 70.2 57.2682 70.2 46.6437 70.2C45.3864 70.2 44.8665 68.8141 45.0289 67.7529C46.1693 60.2998 52.4354 54.6 59.9943 54.6C67.5533 54.6 73.8194 60.2998 74.9598 67.7529C75.1996 69.3204 74.3541 70.2 72.7451 70.2Z" stroke="#CED4DC" stroke-width="2"/>
<path d="M73.557 103.195C74.3197 104.319 74.3197 105.443 73.557 106.272C73.1462 106.745 72.3835 107.041 71.738 107.041C71.6242 107.041 71.5013 107.032 71.3801 107.011C71.3345 108.505 70.5896 109.217 70.0948 109.467C69.7427 109.703 69.3318 109.822 68.9212 109.822C68.8703 109.822 68.8204 109.82 68.7714 109.816C68.7035 109.811 68.6373 109.803 68.5729 109.792C68.5559 110.332 68.3591 110.827 67.9823 111.242C67.6302 111.833 66.926 112.129 66.2808 112.129C66.1045 112.129 65.9874 112.129 65.8112 112.07C65.7753 112.058 65.7381 112.044 65.6997 112.028C65.5215 112.624 65.0169 113.105 64.227 113.431C63.9924 113.49 63.8162 113.49 63.5815 113.49C62.4417 113.49 60.9477 112.633 60.0352 112.028M55.5 88.7567C53.6867 88.9991 51.6246 89.9258 50.4378 90.4744C50.2985 90.5095 50.1797 90.5655 50.0693 90.6176L50.0687 90.6179C49.9937 90.6534 49.9223 90.6871 49.851 90.711C49.5281 90.5373 49.0708 90.3955 48.4995 90.2184C47.5139 89.9127 46.1886 89.5016 44.6287 88.6402C44.3941 88.5219 44.0419 88.581 43.8657 88.8769L37.6461 99.5268C37.4699 99.8226 37.5287 100.178 37.822 100.355L46.8 105.019" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M73.7624 103.371C73.6448 103.371 73.4683 103.313 73.3507 103.195L67.7625 97.9108C63.1743 96.7365 61.8803 93.9181 61.4097 92.8024C61.0567 92.8612 60.5273 92.9199 59.645 92.9199C59.2332 93.2135 58.1744 94.0355 56.7039 94.7988C55.3509 95.5034 53.998 95.2098 53.4098 94.2117C53.0568 93.5658 52.998 92.8024 53.351 92.1566C55.7039 87.5766 58.4685 86.6372 60.3509 86.6372C61.4685 86.6372 62.1744 86.9895 62.2332 86.9895L69.2919 90.1015L75.7624 87.1656C75.8801 87.1069 76.0565 87.1069 76.233 87.1656C76.4095 87.2243 76.5271 87.3418 76.5859 87.4592L82.6447 97.8521C82.7623 98.1457 82.6447 98.4393 82.4094 98.6154L74.0565 103.313C73.9389 103.371 73.8801 103.371 73.7624 103.371Z" stroke="#CED4DC" stroke-width="2"/>
<path d="M46.6097 107.206C47.0221 108.443 48.394 109.005 49.044 109.195C49.217 109.923 49.5794 110.391 49.9178 110.692C50.5086 111.164 51.1584 111.342 51.7491 111.342C51.9248 111.342 52.0796 111.321 52.2135 111.291C52.2516 111.368 52.2936 111.444 52.3399 111.519C52.8716 112.346 53.8168 112.818 55.2346 112.937C55.3143 112.937 55.3897 112.919 55.4573 112.887C56.0276 113.588 56.9845 114 57.893 114C58.7791 114 59.488 113.527 59.7243 112.759C60.0787 111.223 59.9606 109.628 59.7243 108.742C59.5602 108.031 59.1935 106.763 57.6392 106.673C57.4017 106.121 56.803 105.205 55.4709 105.08C55.1879 105.053 54.9235 105.086 54.6774 105.164C54.4496 104.62 54.0132 103.917 53.2851 103.662C52.7534 103.485 51.9264 103.662 51.1584 104.134C51.1181 104.161 51.0782 104.187 51.0387 104.215C50.6899 103.795 50.1003 103.263 49.3271 103.189C48.6182 103.13 48.0275 103.485 47.4367 104.193C46.5506 105.316 46.3143 106.32 46.6097 107.206Z" stroke="#CED4DC" stroke-width="2"/>
<path d="M51.9 104.4C51.5709 104.603 49.9229 105.531 49.5 106.8C49.2 107.7 49.5 108.9 49.8 109.5" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M54.6703 105.317C54.1767 105.571 53.0709 106.485 52.597 108.109C52.1231 109.734 52.3995 110.816 52.597 111.155" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M57.2 107.263C56.7063 107.517 56.051 108.431 55.7104 109.598C55.3697 110.766 55.5129 112.179 55.7104 112.517" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M65.5675 111.739C64.0108 110.96 61.2866 108.431 61.2866 108.431" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M68.2914 109.598C66.1686 108.591 62.4538 105.317 62.4538 105.317" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M71.4049 106.874C68.9991 105.775 64.789 102.204 64.789 102.204" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M86.9971 91.8C95.5145 86.0616 101.962 77.753 105.392 68.094C108.822 58.435 109.055 47.9345 106.056 38.134C103.057 28.3336 96.9838 19.7494 88.7289 13.6419C80.474 7.53438 70.4717 4.22518 60.1907 4.20014C49.9096 4.1751 39.8913 7.43554 31.6065 13.5028C23.3217 19.5701 17.2069 28.1245 14.1599 37.9102C11.1128 47.696 11.294 58.1975 14.6768 67.8731C18.0596 77.5486 24.4658 85.8885 32.955 91.6684" stroke="#CED4DC" stroke-width="2" stroke-linecap="round"/>
<path d="M84.6771 88.2C92.4374 82.9725 98.3114 75.4037 101.437 66.6048C104.562 57.8059 104.774 48.2403 102.042 39.3125C99.3091 30.3847 93.7761 22.5649 86.255 17.0012C78.7338 11.4375 69.6207 8.42293 60.2535 8.40012C50.8863 8.37731 41.7585 11.3474 34.2101 16.8745C26.6618 22.4015 21.0905 30.1942 18.3143 39.1086C15.5381 48.023 15.7032 57.5895 18.7853 66.4035C21.8674 75.2176 27.7042 82.8149 35.4388 88.0801" stroke="#CED4DC" stroke-linecap="round"/>
</svg>
`;
@Component({
selector: "app-no-invoices",
template: `<div class="tw-flex tw-flex-col tw-items-center tw-text-info">
<bit-icon [icon]="icon"></bit-icon>
<p class="tw-mt-4">{{ "noInvoicesToList" | i18n }}</p>
</div>`,
})
export class NoInvoicesComponent {
icon = partnerTrustIcon;
}

View File

@@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import {
AddAccountCreditDialogComponent,
InvoicesComponent,
NoInvoicesComponent,
ManageTaxInformationComponent,
SelectPaymentMethodComponent,
VerifyBankAccountComponent,
@@ -78,6 +79,7 @@ import { IconComponent } from "./vault/components/icon.component";
IconButtonModule,
IconModule,
LinkModule,
IconModule,
],
declarations: [
A11yInvalidDirective,
@@ -109,6 +111,7 @@ import { IconComponent } from "./vault/components/icon.component";
FingerprintPipe,
AddAccountCreditDialogComponent,
InvoicesComponent,
NoInvoicesComponent,
ManageTaxInformationComponent,
SelectPaymentMethodComponent,
VerifyBankAccountComponent,
@@ -145,6 +148,7 @@ import { IconComponent } from "./vault/components/icon.component";
FingerprintPipe,
AddAccountCreditDialogComponent,
InvoicesComponent,
NoInvoicesComponent,
ManageTaxInformationComponent,
SelectPaymentMethodComponent,
VerifyBankAccountComponent,

View File

@@ -970,7 +970,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: LoginEmailServiceAbstraction,
useClass: LoginEmailService,
deps: [StateProvider],
deps: [AccountServiceAbstraction, AuthServiceAbstraction, StateProvider],
}),
safeProvider({
provide: OrgDomainInternalServiceAbstraction,

View File

@@ -2,36 +2,40 @@ import { Observable } from "rxjs";
export abstract class LoginEmailServiceAbstraction {
/**
* An observable that monitors the storedEmail
* An observable that monitors the storedEmail on disk.
* This will return null if an account is being added.
*/
storedEmail$: Observable<string>;
storedEmail$: Observable<string | null>;
/**
* Gets the current email being used in the login process.
* Gets the current email being used in the login process from memory.
* @returns A string of the email.
*/
getEmail: () => string;
/**
* Sets the current email being used in the login process.
* Sets the current email being used in the login process in memory.
* @param email The email to be set.
*/
setEmail: (email: string) => void;
/**
* Gets whether or not the email should be stored on disk.
* Gets from memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
* @returns A boolean stating whether or not the email should be stored on disk.
*/
getRememberEmail: () => boolean;
/**
* Sets whether or not the email should be stored on disk.
* Sets in memory whether or not the email should be stored on disk when
* `saveEmailSettings` is called.
*/
setRememberEmail: (value: boolean) => void;
/**
* Sets the email and rememberEmail properties to null.
* Sets the email and rememberEmail properties in memory to null.
*/
clearValues: () => void;
/**
* - If rememberEmail is true, sets the storedEmail on disk to the current email.
* - If rememberEmail is false, sets the storedEmail on disk to null.
* - Then sets the email and rememberEmail properties to null.
* Saves or clears the email on disk
* - If an account is being added, only changes the stored email when rememberEmail is true.
* - If rememberEmail is true, sets the email on disk to the current email.
* - If rememberEmail is false, sets the email on disk to null.
* Always clears the email and rememberEmail properties from memory.
* @returns A promise that resolves once the email settings are saved.
*/
saveEmailSettings: () => Promise<void>;

View File

@@ -0,0 +1,150 @@
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import {
FakeAccountService,
FakeGlobalState,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { LoginEmailService, STORED_EMAIL } from "./login-email.service";
describe("LoginEmailService", () => {
let sut: LoginEmailService;
let accountService: FakeAccountService;
let authService: MockProxy<AuthService>;
let stateProvider: FakeStateProvider;
const userId = "USER_ID" as UserId;
let storedEmailState: FakeGlobalState<string>;
let mockAuthStatuses$: BehaviorSubject<Record<UserId, AuthenticationStatus>>;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
authService = mock<AuthService>();
stateProvider = new FakeStateProvider(accountService);
storedEmailState = stateProvider.global.getFake(STORED_EMAIL);
mockAuthStatuses$ = new BehaviorSubject<Record<UserId, AuthenticationStatus>>({});
authService.authStatuses$ = mockAuthStatuses$;
sut = new LoginEmailService(accountService, authService, stateProvider);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("storedEmail$", () => {
it("returns the stored email when not adding an account", async () => {
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const result = await firstValueFrom(sut.storedEmail$);
expect(result).toEqual("userEmail@bitwarden.com");
});
it("returns the stored email when not adding an account and the user has just logged in", async () => {
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked });
// account service already initialized with userId as active user
const result = await firstValueFrom(sut.storedEmail$);
expect(result).toEqual("userEmail@bitwarden.com");
});
it("returns null when adding an account", async () => {
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
mockAuthStatuses$.next({
[userId]: AuthenticationStatus.Unlocked,
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
const result = await firstValueFrom(sut.storedEmail$);
expect(result).toBeNull();
});
});
describe("saveEmailSettings", () => {
it("saves the email when not adding an account", async () => {
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const result = await firstValueFrom(storedEmailState.state$);
expect(result).toEqual("userEmail@bitwarden.com");
});
it("clears the email when not adding an account and rememberEmail is false", async () => {
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
const result = await firstValueFrom(storedEmailState.state$);
expect(result).toBeNull();
});
it("saves the email when adding an account", async () => {
mockAuthStatuses$.next({
[userId]: AuthenticationStatus.Unlocked,
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const result = await firstValueFrom(storedEmailState.state$);
expect(result).toEqual("userEmail@bitwarden.com");
});
it("does not clear the email when adding an account and rememberEmail is false", async () => {
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
mockAuthStatuses$.next({
[userId]: AuthenticationStatus.Unlocked,
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
const result = await firstValueFrom(storedEmailState.state$);
// result should not be null
expect(result).toEqual("initialEmail@bitwarden.com");
});
it("clears the email and rememberEmail after saving", async () => {
sut.setEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const result = sut.getEmail();
expect(result).toBeNull();
});
});
});

View File

@@ -1,4 +1,8 @@
import { Observable } from "rxjs";
import { Observable, firstValueFrom, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import {
GlobalState,
@@ -8,20 +12,49 @@ import {
} from "../../../../../common/src/platform/state";
import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service";
const STORED_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_DISK, "storedEmail", {
export const STORED_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_DISK, "storedEmail", {
deserializer: (value: string) => value,
});
export class LoginEmailService implements LoginEmailServiceAbstraction {
private email: string;
private email: string | null;
private rememberEmail: boolean;
private readonly storedEmailState: GlobalState<string>;
storedEmail$: Observable<string>;
// True if an account is currently being added through account switching
private readonly addingAccount$: Observable<boolean>;
constructor(private stateProvider: StateProvider) {
private readonly storedEmailState: GlobalState<string>;
storedEmail$: Observable<string | null>;
constructor(
private accountService: AccountService,
private authService: AuthService,
private stateProvider: StateProvider,
) {
this.storedEmailState = this.stateProvider.getGlobal(STORED_EMAIL);
this.storedEmail$ = this.storedEmailState.state$;
// In order to determine if an account is being added, we check if any account is not logged out
this.addingAccount$ = this.authService.authStatuses$.pipe(
switchMap(async (statuses) => {
// We don't want to consider the active account since it may have just changed auth status to logged in
// which would make this observable think an account is being added
const activeUser = await firstValueFrom(this.accountService.activeAccount$);
if (activeUser) {
delete statuses[activeUser.id];
}
return Object.values(statuses).some((status) => status !== AuthenticationStatus.LoggedOut);
}),
);
this.storedEmail$ = this.storedEmailState.state$.pipe(
switchMap(async (storedEmail) => {
// When adding an account, we don't show the stored email
if (await firstValueFrom(this.addingAccount$)) {
return null;
}
return storedEmail;
}),
);
}
getEmail() {
@@ -37,16 +70,31 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
}
setRememberEmail(value: boolean) {
this.rememberEmail = value;
this.rememberEmail = value ?? false;
}
clearValues() {
this.email = null;
this.rememberEmail = null;
this.rememberEmail = false;
}
async saveEmailSettings() {
await this.storedEmailState.update(() => (this.rememberEmail ? this.email : null));
const addingAccount = await firstValueFrom(this.addingAccount$);
await this.storedEmailState.update((storedEmail) => {
// If we're adding an account, only overwrite the stored email when rememberEmail is true
if (addingAccount) {
if (this.rememberEmail) {
return this.email;
}
return storedEmail;
}
// Logging in with rememberEmail set to false will clear the stored email
if (this.rememberEmail) {
return this.email;
}
return null;
});
this.clearValues();
}
}

View File

@@ -8,6 +8,8 @@ import {
} from "../src/platform/abstractions/storage.service";
import { StorageOptions } from "../src/platform/models/domain/storage-options";
const INTERNAL_KEY = "__internal__";
export class FakeStorageService implements AbstractStorageService, ObservableStorageService {
private store: Record<string, unknown>;
private updatesSubject = new Subject<StorageUpdate>();
@@ -63,13 +65,32 @@ export class FakeStorageService implements AbstractStorageService, ObservableSto
this.mock.has(key, options);
return Promise.resolve(this.store[key] != null);
}
save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
// These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203
// which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world.
if (typeof key !== "string" && typeof key !== "object") {
throw new TypeError(
`Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`,
);
}
// We don't throw this error because ElectronStorageService automatically detects this case
// and calls `delete()` instead of `set()`.
// if (typeof key !== "object" && obj === undefined) {
// throw new TypeError("Use `delete()` to clear values");
// }
if (this._containsReservedKey(key)) {
throw new TypeError(
`Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`,
);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.mock.save(key, obj, options);
this.store[key] = obj;
this.updatesSubject.next({ key: key, updateType: "save" });
return Promise.resolve();
}
remove(key: string, options?: StorageOptions): Promise<void> {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@@ -79,4 +100,20 @@ export class FakeStorageService implements AbstractStorageService, ObservableSto
this.updatesSubject.next({ key: key, updateType: "remove" });
return Promise.resolve();
}
private _containsReservedKey(key: string | Partial<unknown>): boolean {
if (typeof key === "object") {
const firsKey = Object.keys(key)[0];
if (firsKey === INTERNAL_KEY) {
return true;
}
}
if (typeof key !== "string") {
return false;
}
return false;
}
}

View File

@@ -10,6 +10,8 @@ export class ProviderOrganizationResponse extends BaseResponse {
revisionDate: string;
userCount: number;
seats?: number;
occupiedSeats?: number;
remainingSeats?: number;
plan?: string;
constructor(response: any) {
@@ -23,6 +25,8 @@ export class ProviderOrganizationResponse extends BaseResponse {
this.revisionDate = this.getResponseProperty("RevisionDate");
this.userCount = this.getResponseProperty("UserCount");
this.seats = this.getResponseProperty("Seats");
this.occupiedSeats = this.getResponseProperty("OccupiedSeats");
this.remainingSeats = this.getResponseProperty("RemainingSeats");
this.plan = this.getResponseProperty("Plan");
}
}

View File

@@ -21,6 +21,7 @@ export enum FeatureFlag {
InlineMenuFieldQualification = "inline-menu-field-qualification",
MemberAccessReport = "ac-2059-member-access-report",
EnableTimeThreshold = "PM-5864-dollar-threshold",
GroupsComponentRefactor = "groups-component-refactor",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -52,6 +53,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
[FeatureFlag.MemberAccessReport]: FALSE,
[FeatureFlag.EnableTimeThreshold]: FALSE,
[FeatureFlag.GroupsComponentRefactor]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -791,7 +791,7 @@ export class ApiService implements ApiServiceAbstraction {
true,
true,
);
return new CollectionDetailsResponse(r);
return new CollectionAccessDetailsResponse(r);
}
async putCollection(
@@ -806,7 +806,7 @@ export class ApiService implements ApiServiceAbstraction {
true,
true,
);
return new CollectionDetailsResponse(r);
return new CollectionAccessDetailsResponse(r);
}
async putCollectionUsers(

View File

@@ -92,6 +92,45 @@ describe("MoveDesktopSettings", () => {
global_desktopSettings_browserIntegrationFingerprintEnabled: false,
},
},
{
it: "migrates browser integration without fingerprint enabled",
preMigration: {
global_account_accounts: {
user1: {},
otherUser: {},
},
user1: {
settings: {
minimizeOnCopyToClipboard: false,
},
},
otherUser: {
settings: {
random: "stuff",
},
},
global: {
enableBrowserIntegration: true,
},
},
postMigration: {
global_account_accounts: {
user1: {},
otherUser: {},
},
global: {},
user1: {
settings: {},
},
otherUser: {
settings: {
random: "stuff",
},
},
global_desktopSettings_browserIntegrationEnabled: true,
user_user1_desktopSettings_minimizeOnCopy: false,
},
},
{
it: "does not move non-existant values",
preMigration: {

8
package-lock.json generated
View File

@@ -130,7 +130,7 @@
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "6.10.0",
"electron": "30.1.2",
"electron": "31.1.0",
"electron-builder": "24.13.3",
"electron-log": "5.0.1",
"electron-reload": "2.0.0-alpha.1",
@@ -18439,9 +18439,9 @@
}
},
"node_modules/electron": {
"version": "30.1.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-30.1.2.tgz",
"integrity": "sha512-A5CFGwbA+HSXnzwjc8fP2GIezBcAb0uN/VbNGLOW8DHOYn07rvJ/1bAJECHUUzt5zbfohveG3hpMQiYpbktuDw==",
"version": "31.1.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-31.1.0.tgz",
"integrity": "sha512-TBOwqLxSxnx6+pH6GMri7R3JPH2AkuGJHfWZS0p1HsmN+Qr1T9b0IRJnnehSd/3NZAmAre4ft9Ljec7zjyKFJA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",

View File

@@ -92,7 +92,7 @@
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "6.10.0",
"electron": "30.1.2",
"electron": "31.1.0",
"electron-builder": "24.13.3",
"electron-log": "5.0.1",
"electron-reload": "2.0.0-alpha.1",