mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 06:54:07 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2024.8.1",
|
||||
"version": "2024.8.2",
|
||||
"scripts": {
|
||||
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
||||
"build:mv2": "webpack",
|
||||
|
||||
@@ -86,10 +86,8 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await super.ngOnInit();
|
||||
if (this.showPasswordless) {
|
||||
const loginEmail = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
this.formGroup.controls.email.setValue(loginEmail);
|
||||
this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail());
|
||||
await this.validateEmail();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,7 +652,7 @@ export default class MainBackground {
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.appIdService = new AppIdService(this.globalStateProvider);
|
||||
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
this.organizationService = new OrganizationService(this.stateProvider);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2024.8.1",
|
||||
"version": "2024.8.2",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2024.8.1",
|
||||
"version": "2024.8.2",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -59,6 +59,7 @@ import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/pass
|
||||
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
|
||||
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
|
||||
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
||||
import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component";
|
||||
import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component";
|
||||
import { SendV2Component } from "../tools/popup/send-v2/send-v2.component";
|
||||
import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component";
|
||||
@@ -362,18 +363,16 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { state: "send-type" },
|
||||
},
|
||||
{
|
||||
...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, {
|
||||
path: "add-send",
|
||||
component: SendAddEditComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { state: "add-send" },
|
||||
},
|
||||
{
|
||||
}),
|
||||
...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, {
|
||||
path: "edit-send",
|
||||
component: SendAddEditComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { state: "edit-send" },
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "send-created",
|
||||
component: SendCreatedComponent,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="headerText" showBackButton></popup-header>
|
||||
|
||||
<tools-send-form
|
||||
formId="sendForm"
|
||||
[config]="config"
|
||||
(sendSaved)="onSendSaved()"
|
||||
[submitBtn]="submitBtn"
|
||||
>
|
||||
</tools-send-form>
|
||||
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="submit" form="sendForm" buttonType="primary" #submitBtn>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
@@ -0,0 +1,141 @@
|
||||
import { CommonModule, Location } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Params } from "@angular/router";
|
||||
import { map, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendId } from "@bitwarden/common/types/guid";
|
||||
import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components";
|
||||
import {
|
||||
DefaultSendFormConfigService,
|
||||
SendFormConfig,
|
||||
SendFormConfigService,
|
||||
SendFormMode,
|
||||
} from "@bitwarden/send-ui";
|
||||
|
||||
import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module";
|
||||
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
/**
|
||||
* Helper class to parse query parameters for the AddEdit route.
|
||||
*/
|
||||
class QueryParams {
|
||||
constructor(params: Params) {
|
||||
this.sendId = params.sendId;
|
||||
this.type = parseInt(params.type, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* The ID of the send to edit, empty when it's a new Send
|
||||
*/
|
||||
sendId?: SendId;
|
||||
|
||||
/**
|
||||
* The type of send to create.
|
||||
*/
|
||||
type: SendType;
|
||||
}
|
||||
|
||||
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
|
||||
/**
|
||||
* Component for adding or editing a send item.
|
||||
*/
|
||||
@Component({
|
||||
selector: "tools-send-add-edit",
|
||||
templateUrl: "send-add-edit.component.html",
|
||||
standalone: true,
|
||||
providers: [{ provide: SendFormConfigService, useClass: DefaultSendFormConfigService }],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchModule,
|
||||
JslibModule,
|
||||
FormsModule,
|
||||
ButtonModule,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupFooterComponent,
|
||||
SendFormModule,
|
||||
AsyncActionsModule,
|
||||
],
|
||||
})
|
||||
export class SendAddEditComponent {
|
||||
/**
|
||||
* The header text for the component.
|
||||
*/
|
||||
headerText: string;
|
||||
|
||||
/**
|
||||
* The configuration for the send form.
|
||||
*/
|
||||
config: SendFormConfig;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private location: Location,
|
||||
private i18nService: I18nService,
|
||||
private addEditFormConfigService: SendFormConfigService,
|
||||
) {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when the send is saved.
|
||||
*/
|
||||
onSendSaved() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the route query parameters and builds the configuration based on the parameters.
|
||||
*/
|
||||
subscribeToParams(): void {
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
map((params) => new QueryParams(params)),
|
||||
switchMap(async (params) => {
|
||||
let mode: SendFormMode;
|
||||
if (params.sendId == null) {
|
||||
mode = "add";
|
||||
} else {
|
||||
mode = "edit";
|
||||
}
|
||||
const config = await this.addEditFormConfigService.buildConfig(
|
||||
mode,
|
||||
params.sendId,
|
||||
params.type,
|
||||
);
|
||||
return config;
|
||||
}),
|
||||
)
|
||||
.subscribe((config) => {
|
||||
this.config = config;
|
||||
this.headerText = this.getHeaderText(config.mode, config.sendType);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the header text based on the mode and type.
|
||||
* @param mode The mode of the send form.
|
||||
* @param type The type of the send form.
|
||||
* @returns The header text.
|
||||
*/
|
||||
private getHeaderText(mode: SendFormMode, type: SendType) {
|
||||
const headerKey =
|
||||
mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||
|
||||
switch (type) {
|
||||
case SendType.Text:
|
||||
return this.i18nService.t(headerKey, this.i18nService.t("sendTypeText"));
|
||||
case SendType.File:
|
||||
return this.i18nService.t(headerKey, this.i18nService.t("sendTypeFile"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,7 +413,7 @@ export class ServiceContainer {
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.appIdService = new AppIdService(this.globalStateProvider);
|
||||
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||
|
||||
const customUserAgent =
|
||||
"Bitwarden_CLI/" +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2024.8.3",
|
||||
"version": "2024.9.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2024.8.3",
|
||||
"version": "2024.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2024.8.3",
|
||||
"version": "2024.9.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2024.8.3",
|
||||
"version": "2024.9.0",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.8.2",
|
||||
"version": "2024.8.3",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { PaymentMethodComponent } from "../shared";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { PremiumComponent } from "./premium.component";
|
||||
import { PremiumV2Component } from "./premium/premium-v2.component";
|
||||
import { PremiumComponent } from "./premium/premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
|
||||
@@ -20,11 +24,15 @@ const routes: Routes = [
|
||||
component: UserSubscriptionComponent,
|
||||
data: { titleId: "premiumMembership" },
|
||||
},
|
||||
{
|
||||
path: "premium",
|
||||
component: PremiumComponent,
|
||||
data: { titleId: "goPremium" },
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: PremiumComponent,
|
||||
flaggedComponent: PremiumV2Component,
|
||||
featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
routeOptions: {
|
||||
path: "premium",
|
||||
data: { titleId: "goPremium" },
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "payment-method",
|
||||
component: PaymentMethodComponent,
|
||||
|
||||
@@ -5,7 +5,8 @@ import { BillingSharedModule } from "../shared";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
|
||||
import { PremiumComponent } from "./premium.component";
|
||||
import { PremiumV2Component } from "./premium/premium-v2.component";
|
||||
import { PremiumComponent } from "./premium/premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
|
||||
@@ -16,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
BillingHistoryViewComponent,
|
||||
UserSubscriptionComponent,
|
||||
PremiumComponent,
|
||||
PremiumV2Component,
|
||||
],
|
||||
})
|
||||
export class IndividualBillingModule {}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<bit-section>
|
||||
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
>
|
||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||
</bit-callout>
|
||||
<bit-callout type="success">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<ul class="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpStorage" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpEmergency" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpReports" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTotp" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpSupport" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpFuture" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
>
|
||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
bitButton
|
||||
href="{{ premiumURL }}}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
*ngIf="isSelfHost"
|
||||
>
|
||||
{{ "purchasePremium" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{
|
||||
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||
}}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelected($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</bit-section>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [bitSubmit]="submitPayment">
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="additionalStorage"
|
||||
type="number"
|
||||
step="1"
|
||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||
/>
|
||||
<bit-hint>{{
|
||||
"additionalStorageIntervalDesc"
|
||||
| i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB ×
|
||||
{{ storageGBPrice | currency: "$" }} =
|
||||
{{ additionalStorageCost | currency: "$" }}
|
||||
<hr class="tw-my-3" />
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||
<app-payment-v2 [showBankAccount]="false"></app-payment-v2>
|
||||
<app-tax-info></app-tax-info>
|
||||
<div class="tw-mb-4">
|
||||
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
||||
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
|
||||
<!-- TODO: Currently incorrect - https://bitwarden.atlassian.net/browse/PM-11525 -->
|
||||
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
||||
<p bitTypography="body1">
|
||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
||||
</p>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
</form>
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Component, ViewChild } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, concatMap, from, Observable, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentV2Component } from "../../shared/payment/payment-v2.component";
|
||||
import { TaxInfoComponent } from "../../shared/tax-info.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./premium-v2.component.html",
|
||||
})
|
||||
export class PremiumV2Component {
|
||||
@ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component;
|
||||
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
|
||||
protected addOnFormGroup = new FormGroup({
|
||||
additionalStorage: new FormControl<number>(0, [Validators.min(0), Validators.max(99)]),
|
||||
});
|
||||
|
||||
protected licenseFormGroup = new FormGroup({
|
||||
file: new FormControl<File>(null, [Validators.required]),
|
||||
});
|
||||
|
||||
protected cloudWebVaultURL: string;
|
||||
protected isSelfHost = false;
|
||||
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
protected readonly premiumPrice = 10;
|
||||
protected readonly storageGBPrice = 4;
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private tokenService: TokenService,
|
||||
) {
|
||||
this.isSelfHost = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.hasPremiumFromAnyOrganization$ =
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$;
|
||||
|
||||
combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumPersonally$,
|
||||
this.environmentService.cloudWebVaultUrl$,
|
||||
])
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => {
|
||||
if (hasPremiumPersonally) {
|
||||
return from(this.navigateToSubscriptionPage());
|
||||
}
|
||||
|
||||
this.cloudWebVaultURL = cloudWebVaultURL;
|
||||
return of(true);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
finalizeUpgrade = async () => {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("premiumUpdated"),
|
||||
});
|
||||
await this.navigateToSubscriptionPage();
|
||||
};
|
||||
|
||||
navigateToSubscriptionPage = (): Promise<boolean> =>
|
||||
this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
|
||||
|
||||
onLicenseFileSelected = (event: Event): void => {
|
||||
const element = event.target as HTMLInputElement;
|
||||
this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null;
|
||||
};
|
||||
|
||||
submitPremiumLicense = async (): Promise<void> => {
|
||||
this.licenseFormGroup.markAllAsTouched();
|
||||
|
||||
if (this.licenseFormGroup.invalid) {
|
||||
return this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("selectFile"),
|
||||
});
|
||||
}
|
||||
|
||||
const emailVerified = await this.tokenService.getEmailVerified();
|
||||
if (!emailVerified) {
|
||||
return this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("verifyEmailFirst"),
|
||||
});
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("license", this.licenseFormGroup.value.file);
|
||||
|
||||
await this.apiService.postAccountLicense(formData);
|
||||
await this.finalizeUpgrade();
|
||||
};
|
||||
|
||||
submitPayment = async (): Promise<void> => {
|
||||
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
|
||||
if (this.taxInfoComponent.taxFormGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("paymentMethodType", type.toString());
|
||||
formData.append("paymentToken", token);
|
||||
formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString());
|
||||
formData.append("country", this.taxInfoComponent.country);
|
||||
formData.append("postalCode", this.taxInfoComponent.postalCode);
|
||||
|
||||
await this.apiService.postPremium(formData);
|
||||
await this.finalizeUpgrade();
|
||||
};
|
||||
|
||||
protected get additionalStorageCost(): number {
|
||||
return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage;
|
||||
}
|
||||
|
||||
protected get estimatedTax(): number {
|
||||
return this.taxInfoComponent?.taxRate != null
|
||||
? (this.taxInfoComponent.taxRate / 100) * this.subtotal
|
||||
: 0;
|
||||
}
|
||||
|
||||
protected get premiumURL(): string {
|
||||
return `${this.cloudWebVaultURL}/#/settings/subscription/premium`;
|
||||
}
|
||||
|
||||
protected get subtotal(): number {
|
||||
return this.premiumPrice + this.additionalStorageCost;
|
||||
}
|
||||
|
||||
protected get total(): number {
|
||||
return this.subtotal + this.estimatedTax;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
<form [formGroup]="licenseForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
@@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent, TaxInfoComponent } from "../shared";
|
||||
import { PaymentComponent, TaxInfoComponent } from "../../shared";
|
||||
|
||||
@Component({
|
||||
templateUrl: "premium.component.html",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
|
||||
@@ -11,7 +11,7 @@ import { ToastService } from "@bitwarden/components";
|
||||
selector: "app-adjust-subscription",
|
||||
templateUrl: "adjust-subscription.component.html",
|
||||
})
|
||||
export class AdjustSubscription {
|
||||
export class AdjustSubscription implements OnInit, OnDestroy {
|
||||
@Input() organizationId: string;
|
||||
@Input() maxAutoscaleSeats: number;
|
||||
@Input() currentSeatCount: number;
|
||||
@@ -19,6 +19,8 @@ export class AdjustSubscription {
|
||||
@Input() interval = "year";
|
||||
@Output() onAdjusted = new EventEmitter();
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
adjustSubscriptionForm = this.formBuilder.group({
|
||||
newSeatCount: [0, [Validators.min(0)]],
|
||||
limitSubscription: [false],
|
||||
@@ -30,30 +32,25 @@ export class AdjustSubscription {
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private formBuilder: FormBuilder,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.adjustSubscriptionForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
const maxAutoscaleSeatsControl = this.adjustSubscriptionForm.controls.newMaxSeats;
|
||||
|
||||
if (value.limitSubscription) {
|
||||
maxAutoscaleSeatsControl.setValidators([Validators.min(value.newSeatCount)]);
|
||||
maxAutoscaleSeatsControl.enable({ emitEvent: false });
|
||||
} else {
|
||||
maxAutoscaleSeatsControl.disable({ emitEvent: false });
|
||||
}
|
||||
});
|
||||
|
||||
this.adjustSubscriptionForm.patchValue({
|
||||
newSeatCount: this.currentSeatCount,
|
||||
limitSubscription: this.maxAutoscaleSeats != null,
|
||||
newMaxSeats: this.maxAutoscaleSeats,
|
||||
limitSubscription: this.maxAutoscaleSeats != null,
|
||||
});
|
||||
this.adjustSubscriptionForm
|
||||
.get("limitSubscription")
|
||||
.valueChanges.pipe(takeUntilDestroyed())
|
||||
.subscribe((value: boolean) => {
|
||||
if (value) {
|
||||
this.adjustSubscriptionForm
|
||||
.get("newMaxSeats")
|
||||
.addValidators([
|
||||
Validators.min(
|
||||
this.adjustSubscriptionForm.value.newSeatCount == null
|
||||
? 1
|
||||
: this.adjustSubscriptionForm.value.newSeatCount,
|
||||
),
|
||||
Validators.required,
|
||||
]);
|
||||
}
|
||||
this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
@@ -99,4 +96,9 @@ export class AdjustSubscription {
|
||||
get limitSubscription(): boolean {
|
||||
return this.adjustSubscriptionForm.value.limitSubscription;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
|
||||
UpdateLicenseDialogComponent,
|
||||
OffboardingSurveyComponent,
|
||||
VerifyBankAccountComponent,
|
||||
PaymentV2Component,
|
||||
],
|
||||
})
|
||||
export class BillingSharedModule {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="updateLicenseForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label *ngIf="showAutomaticSyncAndManualUpload">{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<div class="tw-pb-1 tw-pt-2">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -32,6 +32,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
@@ -41,6 +42,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
@@ -207,6 +209,11 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AppIdService,
|
||||
useClass: DefaultAppIdService,
|
||||
deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
import { take, takeUntil } from "rxjs/operators";
|
||||
import { Subject, firstValueFrom, of } from "rxjs";
|
||||
import { switchMap, take, takeUntil } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
@@ -99,20 +99,31 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
this.route?.queryParams
|
||||
.pipe(
|
||||
switchMap((params) => {
|
||||
if (!params) {
|
||||
// If no params,loadEmailSettings from state
|
||||
return this.loadEmailSettings();
|
||||
}
|
||||
|
||||
const queryParamsEmail = params.email;
|
||||
const queryParamsEmail = params.email;
|
||||
|
||||
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(queryParamsEmail);
|
||||
this.paramEmailSet = true;
|
||||
}
|
||||
});
|
||||
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(queryParamsEmail);
|
||||
this.paramEmailSet = true;
|
||||
}
|
||||
|
||||
if (!this.paramEmailSet) {
|
||||
// If paramEmailSet is false, loadEmailSettings from state
|
||||
return this.paramEmailSet ? of(null) : this.loadEmailSettings();
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.route) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: AppIdServiceAbstraction,
|
||||
useClass: AppIdService,
|
||||
deps: [GlobalStateProvider],
|
||||
deps: [OBSERVABLE_DISK_STORAGE, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuditServiceAbstraction,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class AppIdService {
|
||||
abstract appId$: Observable<string>;
|
||||
abstract anonymousAppId$: Observable<string>;
|
||||
abstract getAppId(): Promise<string>;
|
||||
abstract getAnonymousAppId(): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { FakeGlobalState, FakeGlobalStateProvider, ObservableTracker } from "../../../spec";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { FakeStorageService } from "../../../spec";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
import { ANONYMOUS_APP_ID_KEY, APP_ID_KEY, AppIdService } from "./app-id.service";
|
||||
|
||||
describe("AppIdService", () => {
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let appIdState: FakeGlobalState<string>;
|
||||
let anonymousAppIdState: FakeGlobalState<string>;
|
||||
let fakeStorageService: FakeStorageService;
|
||||
let sut: AppIdService;
|
||||
|
||||
beforeEach(() => {
|
||||
globalStateProvider = new FakeGlobalStateProvider();
|
||||
appIdState = globalStateProvider.getFake(APP_ID_KEY);
|
||||
anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY);
|
||||
sut = new AppIdService(globalStateProvider);
|
||||
fakeStorageService = new FakeStorageService();
|
||||
sut = new AppIdService(fakeStorageService, mock<LogService>());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -22,7 +21,7 @@ describe("AppIdService", () => {
|
||||
|
||||
describe("getAppId", () => {
|
||||
it("returns the existing appId when it exists", async () => {
|
||||
appIdState.stateSubject.next("existingAppId");
|
||||
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: "existingAppId" });
|
||||
|
||||
const appId = await sut.getAppId();
|
||||
|
||||
@@ -30,7 +29,7 @@ describe("AppIdService", () => {
|
||||
});
|
||||
|
||||
it("creates a new appId only once", async () => {
|
||||
appIdState.stateSubject.next(null);
|
||||
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: null });
|
||||
|
||||
const appIds: string[] = [];
|
||||
const promises = [async () => appIds.push(await sut.getAppId())];
|
||||
@@ -41,7 +40,7 @@ describe("AppIdService", () => {
|
||||
});
|
||||
|
||||
it.each([null, undefined])("returns a new appId when %s", async (value) => {
|
||||
appIdState.stateSubject.next(value);
|
||||
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: value });
|
||||
|
||||
const appId = await sut.getAppId();
|
||||
|
||||
@@ -49,27 +48,17 @@ describe("AppIdService", () => {
|
||||
});
|
||||
|
||||
it.each([null, undefined])("stores the new guid when %s", async (value) => {
|
||||
appIdState.stateSubject.next(value);
|
||||
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: value });
|
||||
|
||||
const appId = await sut.getAppId();
|
||||
|
||||
expect(appIdState.nextMock).toHaveBeenCalledWith(appId);
|
||||
});
|
||||
|
||||
it("emits only once when creating a new appId", async () => {
|
||||
appIdState.stateSubject.next(null);
|
||||
|
||||
const tracker = new ObservableTracker(sut.appId$);
|
||||
const appId = await sut.getAppId();
|
||||
|
||||
expect(tracker.emissions).toEqual([appId]);
|
||||
await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded");
|
||||
expect(fakeStorageService.mock.save).toHaveBeenCalledWith(APP_ID_KEY, appId, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAnonymousAppId", () => {
|
||||
it("returns the existing appId when it exists", async () => {
|
||||
anonymousAppIdState.stateSubject.next("existingAppId");
|
||||
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: "existingAppId" });
|
||||
|
||||
const appId = await sut.getAnonymousAppId();
|
||||
|
||||
@@ -77,7 +66,7 @@ describe("AppIdService", () => {
|
||||
});
|
||||
|
||||
it("creates a new anonymousAppId only once", async () => {
|
||||
anonymousAppIdState.stateSubject.next(null);
|
||||
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: null });
|
||||
|
||||
const appIds: string[] = [];
|
||||
const promises = [async () => appIds.push(await sut.getAnonymousAppId())];
|
||||
@@ -88,7 +77,7 @@ describe("AppIdService", () => {
|
||||
});
|
||||
|
||||
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
|
||||
anonymousAppIdState.stateSubject.next(value);
|
||||
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: value });
|
||||
|
||||
const appId = await sut.getAnonymousAppId();
|
||||
|
||||
@@ -98,22 +87,16 @@ describe("AppIdService", () => {
|
||||
it.each([null, undefined])(
|
||||
"stores the new guid when it an existing one is not found",
|
||||
async (value) => {
|
||||
anonymousAppIdState.stateSubject.next(value);
|
||||
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: value });
|
||||
|
||||
const appId = await sut.getAnonymousAppId();
|
||||
|
||||
expect(anonymousAppIdState.nextMock).toHaveBeenCalledWith(appId);
|
||||
expect(fakeStorageService.mock.save).toHaveBeenCalledWith(
|
||||
ANONYMOUS_APP_ID_KEY,
|
||||
appId,
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("emits only once when creating a new anonymousAppId", async () => {
|
||||
anonymousAppIdState.stateSubject.next(null);
|
||||
|
||||
const tracker = new ObservableTracker(sut.anonymousAppId$);
|
||||
const appId = await sut.getAnonymousAppId();
|
||||
|
||||
expect(tracker.emissions).toEqual([appId]);
|
||||
await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,59 +1,34 @@
|
||||
import { Observable, concatMap, distinctUntilChanged, firstValueFrom, share } from "rxjs";
|
||||
|
||||
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { APPLICATION_ID_DISK, GlobalStateProvider, KeyDefinition } from "../state";
|
||||
|
||||
export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", {
|
||||
deserializer: (value: string) => value,
|
||||
cleanupDelayMs: 0,
|
||||
debug: {
|
||||
enableRetrievalLogging: true,
|
||||
enableUpdateLogging: true,
|
||||
},
|
||||
});
|
||||
export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", {
|
||||
deserializer: (value: string) => value,
|
||||
});
|
||||
export const APP_ID_KEY = "global_applicationId_appId";
|
||||
export const ANONYMOUS_APP_ID_KEY = "global_applicationId_appId";
|
||||
|
||||
export class AppIdService implements AppIdServiceAbstraction {
|
||||
appId$: Observable<string>;
|
||||
anonymousAppId$: Observable<string>;
|
||||
|
||||
constructor(globalStateProvider: GlobalStateProvider) {
|
||||
const appIdState = globalStateProvider.get(APP_ID_KEY);
|
||||
const anonymousAppIdState = globalStateProvider.get(ANONYMOUS_APP_ID_KEY);
|
||||
this.appId$ = appIdState.state$.pipe(
|
||||
concatMap(async (appId) => {
|
||||
if (!appId) {
|
||||
return await appIdState.update(() => Utils.newGuid(), {
|
||||
shouldUpdate: (v) => v == null,
|
||||
});
|
||||
}
|
||||
return appId;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
share(),
|
||||
);
|
||||
this.anonymousAppId$ = anonymousAppIdState.state$.pipe(
|
||||
concatMap(async (appId) => {
|
||||
if (!appId) {
|
||||
return await anonymousAppIdState.update(() => Utils.newGuid(), {
|
||||
shouldUpdate: (v) => v == null,
|
||||
});
|
||||
}
|
||||
return appId;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
constructor(
|
||||
private readonly storageService: AbstractStorageService,
|
||||
private readonly logService: LogService,
|
||||
) {}
|
||||
|
||||
async getAppId(): Promise<string> {
|
||||
return await firstValueFrom(this.appId$);
|
||||
this.logService.info("Retrieving application id");
|
||||
return await this.getEnsuredValue(APP_ID_KEY);
|
||||
}
|
||||
|
||||
async getAnonymousAppId(): Promise<string> {
|
||||
return await firstValueFrom(this.anonymousAppId$);
|
||||
return await this.getEnsuredValue(ANONYMOUS_APP_ID_KEY);
|
||||
}
|
||||
|
||||
private async getEnsuredValue(key: string) {
|
||||
let value = await this.storageService.get<string | null>(key);
|
||||
|
||||
if (value == null) {
|
||||
value = Utils.newGuid();
|
||||
await this.storageService.save(key, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
class:
|
||||
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 tw-rounded-lg tw-py-4 tw-px-3",
|
||||
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg tw-py-4 tw-px-3",
|
||||
},
|
||||
})
|
||||
export class CardComponent {}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { LayoutComponent } from "../layout";
|
||||
import { SectionComponent } from "../section";
|
||||
import { TypographyModule } from "../typography";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { CardComponent } from "./card.component";
|
||||
|
||||
@@ -10,7 +15,20 @@ export default {
|
||||
component: CardComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [TypographyModule, SectionComponent],
|
||||
imports: [TypographyModule, SectionComponent, LayoutComponent, RouterTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
toggleSideNavigation: "Toggle side navigation",
|
||||
skipToContent: "Skip to content",
|
||||
submenu: "submenu",
|
||||
toggleCollapse: "toggle collapse",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
componentWrapperDecorator(
|
||||
(story) => `<div class="tw-bg-background-alt tw-p-10 tw-text-main">${story}</div>`,
|
||||
@@ -60,3 +78,16 @@ export const WithinSections: Story = {
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithoutBorderRadius: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-layout>
|
||||
<bit-card>
|
||||
<p bitTypography="body1" class="!tw-mb-0">Cards used in <code>bit-layout</code> will not have a border radius</p>
|
||||
</bit-card>
|
||||
</bit-layout>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 tw-rounded-lg tw-mb-1.5"
|
||||
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg tw-mb-1.5"
|
||||
[ngClass]="
|
||||
focusVisibleWithin()
|
||||
? 'tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-primary-700 tw-border-transparent'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs";
|
||||
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./item.stories";
|
||||
|
||||
@@ -15,9 +15,17 @@ import { ItemModule } from "@bitwarden/components";
|
||||
It is a generic container that can be used for either standalone content, an alternative to tables,
|
||||
or to list nav links.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.Default} />
|
||||
</Canvas>
|
||||
<Story of={stories.Default} />
|
||||
|
||||
<br />
|
||||
|
||||
Items used within a parent `bit-layout` component will not have a border radius, since the
|
||||
`bit-layout` background is white.
|
||||
|
||||
<Story of={stories.WithoutBorderRadius} />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## Primary Content
|
||||
|
||||
@@ -41,9 +49,7 @@ The content can be a button, anchor, or static container.
|
||||
</bit-item>
|
||||
```
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.ContentTypes} />
|
||||
</Canvas>
|
||||
<Story of={stories.ContentTypes} />
|
||||
|
||||
### Content Slots
|
||||
|
||||
@@ -74,9 +80,7 @@ The content can be a button, anchor, or static container.
|
||||
</bit-item>
|
||||
```
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.ContentSlots} />
|
||||
</Canvas>
|
||||
<Story of={stories.ContentSlots} />
|
||||
|
||||
## Secondary Actions
|
||||
|
||||
@@ -109,13 +113,9 @@ Actions are commonly icon buttons or badge buttons.
|
||||
|
||||
Groups of items can be associated by wrapping them in the `<bit-item-group>`.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.MultipleActionList} />
|
||||
</Canvas>
|
||||
<Story of={stories.MultipleActionList} />
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.SingleActionList} />
|
||||
</Canvas>
|
||||
<Story of={stories.SingleActionList} />
|
||||
|
||||
### A11y
|
||||
|
||||
@@ -136,6 +136,4 @@ Use `aria-label` or `aria-labelledby` to give groups an accessible name.
|
||||
|
||||
### Virtual Scrolling
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.VirtualScrolling} />
|
||||
</Canvas>
|
||||
<Story of={stories.VirtualScrolling} />
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { A11yGridDirective } from "../a11y/a11y-grid.directive";
|
||||
import { AvatarModule } from "../avatar";
|
||||
import { BadgeModule } from "../badge";
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { LayoutComponent } from "../layout";
|
||||
import { TypographyModule } from "../typography";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { ItemActionComponent } from "./item-action.component";
|
||||
import { ItemContentComponent } from "./item-content.component";
|
||||
@@ -29,6 +34,21 @@ export default {
|
||||
ItemContentComponent,
|
||||
A11yGridDirective,
|
||||
ScrollingModule,
|
||||
LayoutComponent,
|
||||
RouterTestingModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
toggleSideNavigation: "Toggle side navigation",
|
||||
skipToContent: "Skip to content",
|
||||
submenu: "submenu",
|
||||
toggleCollapse: "toggle collapse",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
componentWrapperDecorator((story) => `<div class="tw-bg-background-alt tw-p-2">${story}</div>`),
|
||||
@@ -333,3 +353,32 @@ export const VirtualScrolling: Story = {
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithoutBorderRadius: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-layout>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</bit-layout>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<button
|
||||
bit-item-content
|
||||
appA11yTitle="{{ 'edit' | i18n }} - {{ send.name }}"
|
||||
routerLink="/edit-send"
|
||||
[queryParams]="{ sendId: send.id, type: send.type }"
|
||||
appStopClick
|
||||
type="button"
|
||||
class="tw-pb-1"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<li *ngFor="let attachment of cipher.attachments">
|
||||
<bit-item>
|
||||
<bit-item-content>
|
||||
<span data-testid="file-name">{{ attachment.fileName }}</span>
|
||||
<span data-testid="file-name" [title]="attachment.fileName">{{ attachment.fileName }}</span>
|
||||
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
|
||||
</bit-item-content>
|
||||
<ng-container slot="end">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<bit-item-group>
|
||||
<bit-item *ngFor="let attachment of cipher.attachments">
|
||||
<bit-item-content>
|
||||
<span data-testid="file-name">{{ attachment.fileName }}</span>
|
||||
<span data-testid="file-name" [title]="attachment.fileName">{{ attachment.fileName }}</span>
|
||||
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
|
||||
</bit-item-content>
|
||||
<ng-container slot="end">
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -193,7 +193,7 @@
|
||||
},
|
||||
"apps/browser": {
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2024.8.1"
|
||||
"version": "2024.8.2"
|
||||
},
|
||||
"apps/cli": {
|
||||
"name": "@bitwarden/cli",
|
||||
@@ -233,7 +233,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2024.8.3",
|
||||
"version": "2024.9.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
@@ -247,7 +247,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.8.2"
|
||||
"version": "2024.8.3"
|
||||
},
|
||||
"libs/admin-console": {
|
||||
"name": "@bitwarden/admin-console",
|
||||
|
||||
Reference in New Issue
Block a user